org.opencastproject.distribution.aws.s3.AwsS3DistributionServiceImpl Maven / Gradle / Ivy
/*
* 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.distribution.aws.s3;
import static java.lang.String.format;
import static org.opencastproject.util.RequireUtil.notNull;
import org.opencastproject.distribution.api.AbstractDistributionService;
import org.opencastproject.distribution.api.DistributionException;
import org.opencastproject.distribution.api.DistributionService;
import org.opencastproject.distribution.api.DownloadDistributionService;
import org.opencastproject.distribution.aws.s3.api.AwsS3DistributionService;
import org.opencastproject.job.api.Job;
import org.opencastproject.mediapackage.AdaptivePlaylist;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElementParser;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.MediaPackageParser;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UserDirectoryService;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.util.ConfigurationException;
import org.opencastproject.util.LoadUtil;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.OsgiUtil;
import org.opencastproject.util.data.Option;
import org.opencastproject.workspace.api.Workspace;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.auth.policy.Policy;
import com.amazonaws.auth.policy.Principal;
import com.amazonaws.auth.policy.Statement;
import com.amazonaws.auth.policy.actions.S3Actions;
import com.amazonaws.auth.policy.resources.S3ObjectResource;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.BucketWebsiteConfiguration;
import com.amazonaws.services.s3.model.SetBucketWebsiteConfigurationRequest;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.Upload;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletResponse;
@Component(
immediate = true,
service = { DistributionService.class, DownloadDistributionService.class, AwsS3DistributionService.class },
property = {
"service.description=Distribution Service (AWS S3)",
"distribution.channel=aws.s3"
}
)
public class AwsS3DistributionServiceImpl extends AbstractDistributionService
implements AwsS3DistributionService, DistributionService {
/** Logging facility */
private static final Logger logger = LoggerFactory.getLogger(AwsS3DistributionServiceImpl.class);
/** Job type */
public static final String JOB_TYPE = "org.opencastproject.distribution.aws.s3";
/** List of available operations on jobs */
public enum Operation {
Distribute, Retract
}
// Service configuration
public static final String AWS_S3_DISTRIBUTION_ENABLE = "org.opencastproject.distribution.aws.s3.distribution.enable";
public static final String AWS_S3_DISTRIBUTION_BASE_CONFIG
= "org.opencastproject.distribution.aws.s3.distribution.base";
public static final String AWS_S3_ACCESS_KEY_ID_CONFIG = "org.opencastproject.distribution.aws.s3.access.id";
public static final String AWS_S3_SECRET_ACCESS_KEY_CONFIG = "org.opencastproject.distribution.aws.s3.secret.key";
public static final String AWS_S3_REGION_CONFIG = "org.opencastproject.distribution.aws.s3.region";
public static final String AWS_S3_BUCKET_CONFIG = "org.opencastproject.distribution.aws.s3.bucket";
public static final String AWS_S3_ENDPOINT_CONFIG = "org.opencastproject.distribution.aws.s3.endpoint";
public static final String AWS_S3_PATH_STYLE_CONFIG = "org.opencastproject.distribution.aws.s3.path.style";
public static final String AWS_S3_PRESIGNED_URL_CONFIG = "org.opencastproject.distribution.aws.s3.presigned.url";
public static final String AWS_S3_PRESIGNED_URL_VALID_DURATION_CONFIG
= "org.opencastproject.distribution.aws.s3.presigned.url.valid.duration";
// S3 client configuration
public static final String AWS_S3_MAX_CONNECTIONS = "org.opencastproject.distribution.aws.s3.max.connections";
public static final String AWS_S3_CONNECTION_TIMEOUT = "org.opencastproject.distribution.aws.s3.connection.timeout";
public static final String AWS_S3_MAX_RETRIES = "org.opencastproject.distribution.aws.s3.max.retries";
// job loads
public static final String DISTRIBUTE_JOB_LOAD_KEY = "job.load.aws.s3.distribute";
public static final String RETRACT_JOB_LOAD_KEY = "job.load.aws.s3.retract";
// config.properties
public static final String OPENCAST_STORAGE_DIR = "org.opencastproject.storage.dir";
public static final String DEFAULT_TEMP_DIR = "tmp/s3dist";
// Defaults
// S3 client config defaults
public static final int DEFAULT_MAX_CONNECTIONS = 50;
public static final int DEFAULT_CONNECTION_TIMEOUT = 10000;
public static final int DEFAULT_MAX_RETRIES = 100;
/** The load on the system introduced by creating a distribute job */
public static final float DEFAULT_DISTRIBUTE_JOB_LOAD = 0.1f;
/** The load on the system introduced by creating a retract job */
public static final float DEFAULT_RETRACT_JOB_LOAD = 0.1f;
/** Default expiration time for presigned URL in millis, 6 hours */
public static final int DEFAULT_PRESIGNED_URL_EXPIRE_MILLIS = 6 * 60 * 60 * 1000;
/** Max expiration time for presigned URL in millis, 7 days */
private static final int MAXIMUM_PRESIGNED_URL_EXPIRE_MILLIS = 7 * 24 * 60 * 60 * 1000;
/** The load on the system introduced by creating a distribute job */
private float distributeJobLoad = DEFAULT_DISTRIBUTE_JOB_LOAD;
/** The load on the system introduced by creating a retract job */
private float retractJobLoad = DEFAULT_RETRACT_JOB_LOAD;
/** Maximum number of tries for checking availability of distributed file */
private static final int MAX_TRIES = 10;
/** Interval time in millis to sleep between checks of availability */
private static final long SLEEP_INTERVAL = 30000L;
/** The AWS client and transfer manager */
private AmazonS3 s3 = null;
private TransferManager s3TransferManager = null;
/** The AWS S3 bucket name */
private String bucketName = null;
private Path tmpPath = null;
/** The AWS S3 endpoint */
private String endpoint = null;
/** path style enabled */
private boolean pathStyle = false;
/** whether use presigned URL */
private boolean presignedUrl = false;
/** valid duration for presigned URL in milliseconds */
private int presignedUrlValidDuration = DEFAULT_PRESIGNED_URL_EXPIRE_MILLIS;
/** The opencast download distribution url */
private String opencastDistributionUrl = null;
private Gson gson = new Gson();
/**
* Creates a new instance of the AWS S3 distribution service.
*/
public AwsS3DistributionServiceImpl() {
super(JOB_TYPE);
}
private String getAWSConfigKey(ComponentContext cc, String key) {
try {
return OsgiUtil.getComponentContextProperty(cc, key);
} catch (RuntimeException e) {
throw new ConfigurationException(key + " is missing or invalid", e);
}
}
@Override
@Activate
public void activate(ComponentContext cc) {
// Get the configuration
if (cc != null) {
if (!BooleanUtils.toBoolean(getAWSConfigKey(cc, AWS_S3_DISTRIBUTION_ENABLE))) {
logger.info("AWS S3 distribution disabled");
return;
}
tmpPath = Paths.get(cc.getBundleContext().getProperty(OPENCAST_STORAGE_DIR), DEFAULT_TEMP_DIR);
// clean up old data and delete directory if it exists
if (tmpPath.toFile().exists()) {
try (Stream walk = Files.walk(tmpPath)) {
walk.map(Path::toFile).sorted(Comparator.reverseOrder()).forEach(File::delete);
} catch (IOException e) {
logger.warn("Unable to delete {}", tmpPath, e);
}
}
logger.info("AWS S3 Distribution uses temp storage in {}", tmpPath);
try { // create a new temp directory
Files.createDirectories(tmpPath);
} catch (IOException e) {
logger.error("Could not create temporary directory for AWS S3 Distribution : `{}`", tmpPath);
throw new IllegalStateException(e);
}
// AWS S3 bucket name
bucketName = getAWSConfigKey(cc, AWS_S3_BUCKET_CONFIG);
logger.info("AWS S3 bucket name is {}", bucketName);
// AWS region
String regionStr = getAWSConfigKey(cc, AWS_S3_REGION_CONFIG);
logger.info("AWS region is {}", regionStr);
// AWS endpoint
endpoint = OsgiUtil.getComponentContextProperty(cc, AWS_S3_ENDPOINT_CONFIG, "s3." + regionStr + ".amazonaws.com");
logger.info("AWS S3 endpoint is {}", endpoint);
// AWS path style
pathStyle = BooleanUtils.toBoolean(OsgiUtil.getComponentContextProperty(cc, AWS_S3_PATH_STYLE_CONFIG, "false"));
logger.info("AWS path style is {}", pathStyle);
// AWS presigned URL
String presignedUrlConfigValue = OsgiUtil.getComponentContextProperty(cc, AWS_S3_PRESIGNED_URL_CONFIG, "false");
presignedUrl = StringUtils.equalsIgnoreCase("true", presignedUrlConfigValue);
logger.info("AWS use presigned URL: {}", presignedUrl);
// AWS presigned URL expiration time in millis
String presignedUrlExpTimeMillisConfigValue = OsgiUtil.getComponentContextProperty(cc,
AWS_S3_PRESIGNED_URL_VALID_DURATION_CONFIG, null);
presignedUrlValidDuration = NumberUtils.toInt(presignedUrlExpTimeMillisConfigValue,
DEFAULT_PRESIGNED_URL_EXPIRE_MILLIS);
if (presignedUrlValidDuration > MAXIMUM_PRESIGNED_URL_EXPIRE_MILLIS) {
logger.warn(
"Valid duration of presigned URL is too large, MAXIMUM_PRESIGNED_URL_EXPIRE_MILLIS(7 days) is used");
presignedUrlValidDuration = MAXIMUM_PRESIGNED_URL_EXPIRE_MILLIS;
}
opencastDistributionUrl = getAWSConfigKey(cc, AWS_S3_DISTRIBUTION_BASE_CONFIG);
if (!opencastDistributionUrl.endsWith("/")) {
opencastDistributionUrl = opencastDistributionUrl + "/";
}
logger.info("AWS distribution url is {}", opencastDistributionUrl);
distributeJobLoad = LoadUtil.getConfiguredLoadValue(cc.getProperties(), DISTRIBUTE_JOB_LOAD_KEY,
DEFAULT_DISTRIBUTE_JOB_LOAD, serviceRegistry);
retractJobLoad = LoadUtil.getConfiguredLoadValue(cc.getProperties(), RETRACT_JOB_LOAD_KEY,
DEFAULT_RETRACT_JOB_LOAD, serviceRegistry);
// Explicit credentials are optional.
AWSCredentialsProvider provider = null;
Option accessKeyIdOpt = OsgiUtil.getOptCfg(cc.getProperties(), AWS_S3_ACCESS_KEY_ID_CONFIG);
Option accessKeySecretOpt = OsgiUtil.getOptCfg(cc.getProperties(), AWS_S3_SECRET_ACCESS_KEY_CONFIG);
// Keys not informed so use default credentials provider chain, which
// will look at the environment variables, java system props, credential files, and instance
// profile credentials
if (accessKeyIdOpt.isNone() && accessKeySecretOpt.isNone()) {
provider = new DefaultAWSCredentialsProviderChain();
} else {
provider = new AWSStaticCredentialsProvider(
new BasicAWSCredentials(accessKeyIdOpt.get(), accessKeySecretOpt.get()));
}
// S3 client configuration
ClientConfiguration clientConfiguration = new ClientConfiguration();
int maxConnections = OsgiUtil.getOptCfgAsInt(cc.getProperties(), AWS_S3_MAX_CONNECTIONS)
.getOrElse(DEFAULT_MAX_CONNECTIONS);
logger.debug("Max Connections: {}", maxConnections);
clientConfiguration.setMaxConnections(maxConnections);
int connectionTimeout = OsgiUtil.getOptCfgAsInt(cc.getProperties(), AWS_S3_CONNECTION_TIMEOUT)
.getOrElse(DEFAULT_CONNECTION_TIMEOUT);
logger.debug("Connection Output: {}", connectionTimeout);
clientConfiguration.setConnectionTimeout(connectionTimeout);
int maxRetries = OsgiUtil.getOptCfgAsInt(cc.getProperties(), AWS_S3_MAX_RETRIES)
.getOrElse(DEFAULT_MAX_RETRIES);
logger.debug("Max Retry: {}", maxRetries);
clientConfiguration.setMaxErrorRetry(maxRetries);
// Create AWS client
s3 = AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, regionStr))
.withClientConfiguration(clientConfiguration)
.withPathStyleAccessEnabled(pathStyle).withCredentials(provider).build();
s3TransferManager = new TransferManager(s3);
// Create AWS S3 bucket if not there yet
createAWSBucket();
distributionChannel = OsgiUtil.getComponentContextProperty(cc, CONFIG_KEY_STORE_TYPE);
logger.info("AwsS3DistributionService activated!");
}
}
@Override
public String getDistributionType() {
return distributionChannel;
}
@Deactivate
public void deactivate() {
// Transfer manager is null if service disabled
if (s3TransferManager != null) {
s3TransferManager.shutdownNow();
}
logger.info("AwsS3DistributionService deactivated!");
}
@Override
public Job distribute(String pubChannelId, MediaPackage mediaPackage, Set downloadIds,
boolean checkAvailability, boolean preserveReference) throws DistributionException, MediaPackageException {
throw new UnsupportedOperationException("Not supported yet.");
// stub function
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.distribution.api.DownloadDistributionService#distribute(String,
* org.opencastproject.mediapackage.MediaPackage, String, boolean)
*/
@Override
public Job distribute(String channelId, MediaPackage mediaPackage, Set elementIds, boolean checkAvailability)
throws DistributionException, MediaPackageException {
notNull(mediaPackage, "mediapackage");
notNull(elementIds, "elementIds");
notNull(channelId, "channelId");
try {
return serviceRegistry.createJob(JOB_TYPE, Operation.Distribute.toString(), Arrays.asList(channelId,
MediaPackageParser.getAsXml(mediaPackage), gson.toJson(elementIds), Boolean.toString(checkAvailability)),
distributeJobLoad);
} catch (ServiceRegistryException e) {
throw new DistributionException("Unable to create a job", e);
}
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.distribution.api.DistributionService#distribute(String,
* org.opencastproject.mediapackage.MediaPackage, String)
*/
@Override
public Job distribute(String channelId, MediaPackage mediapackage, String elementId)
throws DistributionException, MediaPackageException {
return distribute(channelId, mediapackage, elementId, true);
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.distribution.api.DownloadDistributionService#distribute(String,
* org.opencastproject.mediapackage.MediaPackage, String, boolean)
*/
@Override
public Job distribute(String channelId, MediaPackage mediaPackage, String elementId, boolean checkAvailability)
throws DistributionException, MediaPackageException {
Set elementIds = new HashSet<>();
elementIds.add(elementId);
return distribute(channelId, mediaPackage, elementIds, checkAvailability);
}
/**
* Distribute Mediapackage elements to the download distribution service.
*
* @param channelId
* # The id of the publication channel to be distributed to.
* @param mediapackage
* The media package that contains the elements to be distributed.
* @param elementIds
* The ids of the elements that should be distributed contained within the media package.
* @param checkAvailability
* Check the availability of the distributed element via http.
* @return A reference to the MediaPackageElements that have been distributed.
* @throws DistributionException
* Thrown if the parent directory of the MediaPackageElement cannot be created, if the MediaPackageElement
* cannot be copied or another unexpected exception occurs.
*/
public MediaPackageElement[] distributeElements(String channelId, MediaPackage mediapackage, Set elementIds,
boolean checkAvailability) throws DistributionException {
notNull(mediapackage, "mediapackage");
notNull(elementIds, "elementIds");
notNull(channelId, "channelId");
final Set elements = getElements(mediapackage, elementIds);
List distributedElements = new ArrayList<>();
if (AdaptivePlaylist.hasHLSPlaylist(elements)) {
return distributeHLSElements(channelId, mediapackage, elements, checkAvailability);
}
for (MediaPackageElement element : elements) {
MediaPackageElement distributedElement = distributeElement(channelId, mediapackage, element, checkAvailability);
distributedElements.add(distributedElement);
}
return distributedElements.toArray(new MediaPackageElement[distributedElements.size()]);
}
private Set getElements(MediaPackage mediapackage, Set elementIds)
throws IllegalStateException {
final Set elements = new HashSet<>();
for (String elementId : elementIds) {
MediaPackageElement element = mediapackage.getElementById(elementId);
if (element != null) {
elements.add(element);
} else {
throw new IllegalStateException(
format("No element %s found in mediapackage %s", elementId, mediapackage.getIdentifier()));
}
}
return elements;
}
/**
* Distribute a media package element to AWS S3.
*
* @param mediaPackage
* The media package that contains the element to distribute.
* @param element
* The element that should be distributed contained within the media package.
* @param checkAvailability
* Checks if the distributed element is available
* @return A reference to the MediaPackageElement that has been distributed.
* @throws DistributionException
*/
public MediaPackageElement distributeElement(String channelId, final MediaPackage mediaPackage,
MediaPackageElement element, boolean checkAvailability) throws DistributionException {
notNull(channelId, "channelId");
notNull(mediaPackage, "mediapackage");
notNull(element, "element");
try {
return distributeElement(channelId, mediaPackage, element, checkAvailability, workspace.get(element.getURI()));
} catch (NotFoundException e) {
throw new DistributionException("Unable to find " + element.getURI() + " in the workspace", e);
} catch (IOException e) {
throw new DistributionException("Error loading " + element.getURI() + " from the workspace", e);
}
}
private MediaPackageElement distributeElement(String channelId, final MediaPackage mediaPackage,
MediaPackageElement element, boolean checkAvailability, File source) throws DistributionException {
// Use TransferManager to take advantage of multipart upload.
// TransferManager processes all transfers asynchronously, so this call will return immediately.
try {
String objectName = buildObjectName(channelId, mediaPackage.getIdentifier().toString(), element);
logger.info("Uploading {} to bucket {}...", objectName, bucketName);
Upload upload = s3TransferManager.upload(bucketName, objectName, source);
long start = System.currentTimeMillis();
try {
// Block and wait for the upload to finish
upload.waitForCompletion();
logger.info("Upload of {} to bucket {} completed in {} seconds", objectName, bucketName,
(System.currentTimeMillis() - start) / 1000);
} catch (AmazonClientException e) {
throw new DistributionException("AWS error: " + e.getMessage(), e);
}
// Create a representation of the distributed file in the media package
MediaPackageElement distributedElement = (MediaPackageElement) element.clone();
try {
distributedElement.setURI(getDistributionUri(objectName));
} catch (URISyntaxException e) {
throw new DistributionException("Distributed element produces an invalid URI", e);
}
logger.info("Distributed element {}, object {}", element.getIdentifier(), objectName);
if (checkAvailability) {
URI uri = distributedElement.getURI();
String distributedElementUriStr = uri.toString();
int tries = 0;
CloseableHttpResponse response = null;
boolean success = false;
while (tries < MAX_TRIES) {
try {
if (presignedUrl) {
// 5 minutes should be enough for check availability for presigned URL.
Date fiveMinutesLater = new Date(System.currentTimeMillis() + 5 * 60 * 1000);
uri = s3.generatePresignedUrl(bucketName, objectName, fiveMinutesLater, HttpMethod.HEAD).toURI();
}
CloseableHttpClient httpClient = HttpClients.createDefault();
logger.trace("Trying to access {}", uri);
response = httpClient.execute(new HttpHead(uri));
if (response.getStatusLine().getStatusCode() == HttpServletResponse.SC_OK) {
logger.trace("Successfully got {}", uri);
success = true;
break; // Exit the loop, response is closed
} else {
logger.debug("Http status code when checking distributed element {} is {}", objectName,
response.getStatusLine().getStatusCode());
}
} catch (Exception e) {
logger.info("Checking availability of {} threw exception {}. Trying again.", objectName, e.getMessage());
// Just try again
} finally {
if (null != response) {
response.close();
}
}
tries++;
logger.trace("Sleeping for {} seconds...", SLEEP_INTERVAL / 1000);
Thread.sleep(SLEEP_INTERVAL);
}
if (!success) {
logger.warn("Could not check availability of distributed file {}", uri);
// throw new DistributionException("Unable to load distributed file " + uri.toString());
}
}
return distributedElement;
} catch (Exception e) {
logger.warn("Error distributing element " + element.getIdentifier() + " of media package " + mediaPackage, e);
if (e instanceof DistributionException) {
throw (DistributionException) e;
} else {
throw new DistributionException(e);
}
}
}
@Override
public Job retract(String channelId, MediaPackage mediapackage, String elementId) throws DistributionException {
Set elementIds = new HashSet<>();
elementIds.add(elementId);
return retract(channelId, mediapackage, elementIds);
}
@Override
public Job retract(String channelId, MediaPackage mediapackage, Set elementIds) throws DistributionException {
notNull(mediapackage, "mediapackage");
notNull(elementIds, "elementIds");
notNull(channelId, "channelId");
try {
return serviceRegistry.createJob(JOB_TYPE, Operation.Retract.toString(),
Arrays.asList(channelId, MediaPackageParser.getAsXml(mediapackage), gson.toJson(elementIds)),
retractJobLoad);
} catch (ServiceRegistryException e) {
throw new DistributionException("Unable to create a job", e);
}
}
@Override
public List distributeSync(String channelId, MediaPackage mediapackage, String elementId)
throws DistributionException, MediaPackageException {
Set elementIds = new HashSet();
elementIds.add(elementId);
return distributeSync(channelId, mediapackage, elementIds, true);
}
@Override
public List distributeSync(String channelId, MediaPackage mediapackage, Set elementIds,
boolean checkAvailability) throws DistributionException {
final MediaPackageElement[] distributedElements = distributeElements(channelId, mediapackage, elementIds,
checkAvailability);
if (distributedElements == null) {
return null;
}
return Arrays.asList(distributedElements);
}
@Override
public List retractSync(String channelId, MediaPackage mediapackage, String elementId)
throws DistributionException {
Set elementIds = new HashSet();
elementIds.add(elementId);
return retractSync(channelId, mediapackage, elementIds);
}
@Override
public List retractSync(String channelId, MediaPackage mediaPackage, Set elementIds)
throws DistributionException {
final MediaPackageElement[] retractedElements = retractElements(channelId, mediaPackage, elementIds);
if (retractedElements == null) {
return null;
}
return Arrays.asList(retractedElements);
}
/**
* Retracts the media package element with the given identifier from the distribution channel.
*
* @param channelId
* the channel id
* @param mediaPackage
* the media package
* @param element
* the element
* @return the retracted element or null
if the element was not retracted
*/
protected MediaPackageElement retractElement(String channelId, MediaPackage mediaPackage, MediaPackageElement element)
throws DistributionException {
notNull(mediaPackage, "mediaPackage");
notNull(element, "element");
try {
String objectName = getDistributedObjectName(element);
if (objectName != null) {
s3.deleteObject(bucketName, objectName);
logger.info("Retracted element {}, object {}", element.getIdentifier(), objectName);
}
return element;
} catch (AmazonClientException e) {
throw new DistributionException("AWS error: " + e.getMessage(), e);
}
}
/**
* Retract a media package element from the distribution channel. The retracted element must not necessarily be the
* one given as parameter elementId
. Instead, the element's distribution URI will be calculated. This way
* you are able to retract elements by providing the "original" element here.
*
* @param channelId
* the channel id
* @param mediapackage
* the mediapackage
* @param elementIds
* the element identifiers
* @return the retracted element or null
if the element was not retracted
* @throws org.opencastproject.distribution.api.DistributionException
* in case of an error
*/
protected MediaPackageElement[] retractElements(String channelId, MediaPackage mediapackage, Set elementIds)
throws DistributionException {
notNull(mediapackage, "mediapackage");
notNull(elementIds, "elementIds");
notNull(channelId, "channelId");
Set elements = getElements(mediapackage, elementIds);
List retractedElements = new ArrayList<>();
for (MediaPackageElement element : elements) {
MediaPackageElement retractedElement = retractElement(channelId, mediapackage, element);
retractedElements.add(retractedElement);
}
return retractedElements.toArray(new MediaPackageElement[retractedElements.size()]);
}
/**
* Builds the aws s3 object name.
*
* @param channelId
* @param mpId
* @param element
* @return
*/
protected String buildObjectName(String channelId, String mpId, MediaPackageElement element) {
// Something like ORG_ID/CHANNEL_ID/MP_ID/ELEMENT_ID/FILE_NAME.EXTENSION
final String orgId = securityService.getOrganization().getId();
String uriString = element.getURI().toString();
String fileName = FilenameUtils.getName(uriString);
return buildObjectName(orgId, channelId, mpId, element.getIdentifier(), fileName);
}
/**
* Builds the aws s3 object name using the raw elementID and filename
*
* @param orgId
* @param channelId
* @param mpId
* @param elementId
* @param fileName
* @return
*/
protected String buildObjectName(String orgId, String channelId, String mpId, String elementId, String fileName) {
return StringUtils.join(new String[] { orgId, channelId, mpId, elementId, fileName }, "/");
}
/**
* Gets the URI for the element to be distributed.
*
* @return The resulting URI after distribution
* @throws URISyntaxException
* if the concrete implementation tries to create a malformed uri
*/
protected URI getDistributionUri(String objectName) throws URISyntaxException {
// Something like https://OPENCAST_DOWNLOAD_URL/ORG_ID/CHANNEL_ID/MP_ID/ELEMENT_ID/FILE_NAME.EXTENSION
return new URI(opencastDistributionUrl + objectName);
}
/**
* Gets the distributed object's name.
*
* @return The distributed object name
*/
protected String getDistributedObjectName(MediaPackageElement element) {
// Something like https://OPENCAST_DOWNLOAD_URL/ORG_ID/CHANNEL_ID/MP_ID/ORIGINAL_ELEMENT_ID/FILE_NAME.EXTENSION
String uriString = element.getURI().toString();
// String directoryName = distributionDirectory.getAbsolutePath();
if (uriString.startsWith(opencastDistributionUrl) && uriString.length() > opencastDistributionUrl.length()) {
return uriString.substring(opencastDistributionUrl.length());
} else {
// Cannot retract
logger.warn(
"Cannot retract {}. Uri must be in the format "
+ "https://host/bucketName/orgId/channelId/mpId/originalElementId/fileName.extension",
uriString);
return null;
}
}
/**
* Distribute static items, create a temp directory for playlists, modify them to fix references, then publish the new
* list and then delete the temp files. This is used if there are any HLS playlists in the mediapackage, all the
* videos in the publication should be HLS or progressive, but not both. However, If this is called with non HLS
* files, it will distribute them anyway.
*
* @param channelId
* - distribution channel
* @param mediapackage
* - that holds all the files
* @param elements
* - all the elements for publication
* @param checkAvailability
* - check before pub
* @return distributed elements
* @throws DistributionException
* @throws IOException
*/
private MediaPackageElement[] distributeHLSElements(String channelId, MediaPackage mediapackage,
Set elements, boolean checkAvailability) throws DistributionException {
List distributedElements = new ArrayList();
List nontrackElements = elements.stream()
.filter(e -> e.getElementType() != MediaPackageElement.Type.Track).collect(Collectors.toList());
// Distribute non track items
for (MediaPackageElement element : nontrackElements) {
MediaPackageElement distributedElement = distributeElement(channelId, mediapackage, element, checkAvailability);
distributedElements.add(distributedElement);
}
// Then get all tracks from mediapackage and sort them by flavor
// Each flavor is one video with multiple renditions
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy