/**
* 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.staticfiles.impl;
import static java.lang.String.format;
import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
import static org.opencastproject.util.RequireUtil.notNull;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.staticfiles.api.StaticFileService;
import org.opencastproject.staticfiles.jmx.UploadStatistics;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.OsgiUtil;
import org.opencastproject.util.ProgressInputStream;
import org.opencastproject.util.jmx.JmxUtil;
import com.google.common.util.concurrent.AbstractScheduledService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Service.Listener;
import com.google.common.util.concurrent.Service.State;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.ComponentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.management.ObjectInstance;
/**
* Stores and retrieves static file resources.
*/
public class StaticFileServiceImpl implements StaticFileService {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(StaticFileServiceImpl.class);
/** The key to find the root directory for the static file service in the OSGi properties. */
public static final String STATICFILES_ROOT_DIRECTORY_KEY = "org.opencastproject.staticfiles.rootdir";
/** The JMX business object for uploaded statistics */
private UploadStatistics staticFileStatistics = new UploadStatistics();
/** The JMX bean object instance */
private ObjectInstance registerMXBean;
// OSGi service references
private SecurityService securityService = null;
private OrganizationDirectoryService orgDirectory = null;
/** The root directory for storing static files. */
private String rootDirPath;
private PurgeTemporaryStorageService purgeService;
/**
* OSGI callback for activating this component
*
* @param cc
* the osgi component context
*/
public void activate(ComponentContext cc) {
logger.info("Upload Static Resource Service started.");
registerMXBean = JmxUtil.registerMXBean(staticFileStatistics, "UploadStatistics");
rootDirPath = OsgiUtil.getContextProperty(cc, STATICFILES_ROOT_DIRECTORY_KEY);
final File rootFile = new File(rootDirPath);
if (!rootFile.exists()) {
try {
FileUtils.forceMkdir(rootFile);
} catch (IOException e) {
throw new ComponentException(String.format("%s does not exists and could not be created",
rootFile.getAbsolutePath()));
}
}
if (!rootFile.canRead())
throw new ComponentException(String.format("Cannot read from %s", rootFile.getAbsolutePath()));
purgeService = new PurgeTemporaryStorageService();
purgeService.addListener(new Listener() {
@Override
public void failed(State from, Throwable failure) {
logger.warn("Temporary storage purging service failed: {}", getStackTrace(failure));
}
}, MoreExecutors.sameThreadExecutor());
purgeService.startAsync();
logger.info("Purging of temporary storage section scheduled");
}
/**
* Callback from OSGi on service deactivation.
*/
public void deactivate() {
JmxUtil.unregisterMXBean(registerMXBean);
purgeService.stopAsync();
purgeService = null;
}
/** OSGi DI */
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/** OSGi DI */
public void setOrganizationDirectoryService(OrganizationDirectoryService directoryService) {
this.orgDirectory = directoryService;
}
@Override
public String storeFile(String filename, InputStream inputStream) throws IOException {
notNull(filename, "filename");
notNull(inputStream, "inputStream");
final String uuid = UUID.randomUUID().toString();
final String org = securityService.getOrganization().getId();
Path file = getTemporaryStorageDir(org).resolve(Paths.get(uuid, filename));
try (ProgressInputStream progressInputStream = new ProgressInputStream(inputStream)) {
progressInputStream.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
long totalNumBytesRead = (Long) evt.getNewValue();
long oldTotalNumBytesRead = (Long) evt.getOldValue();
staticFileStatistics.add(totalNumBytesRead - oldTotalNumBytesRead);
}
});
Files.createDirectories(file.getParent());
Files.copy(progressInputStream, file);
} catch (IOException e) {
logger.error("Unable to save file '{}' to {} because: {}",
new Object[] { filename, file, ExceptionUtils.getStackTrace(e) });
throw e;
}
return uuid;
}
@Override
public InputStream getFile(final String uuid) throws NotFoundException, IOException {
if (StringUtils.isBlank(uuid))
throw new IllegalArgumentException("The uuid must not be blank");
final String org = securityService.getOrganization().getId();
return Files.newInputStream(getFile(org, uuid));
}
@Override
public void persistFile(final String uuid) throws NotFoundException, IOException {
final String org = securityService.getOrganization().getId();
try (DirectoryStream<Path> folders = Files.newDirectoryStream(getTemporaryStorageDir(org),
getDirsEqualsUuidFilter(uuid))) {
for (Path folder : folders) {
Files.move(folder, getDurableStorageDir(org).resolve(folder.getFileName()));
}
}
}
@Override
public void deleteFile(String uuid) throws NotFoundException, IOException {
final String org = securityService.getOrganization().getId();
Path file = getFile(org, uuid);
Files.deleteIfExists(file);
}
@Override
public String getFileName(String uuid) throws NotFoundException {
final String org = securityService.getOrganization().getId();
try {
Path file = getFile(org, uuid);
return file.getFileName().toString();
} catch (IOException e) {
logger.warn("Error while reading file: {}", getStackTrace(e));
throw new NotFoundException(e);
}
}
@Override
public Long getContentLength(String uuid) throws NotFoundException {
final String org = securityService.getOrganization().getId();
try {
Path file = getFile(org, uuid);
return Files.size(file);
} catch (IOException e) {
logger.warn("Error while reading file: {}", getStackTrace(e));
throw new NotFoundException(e);
}
}
/**
* Returns a {@link DirectoryStream.Filter} to filter the entries of a directory and only return items which filename
* starts with the UUID.
*
* @param uuid
* The UUID to filter by
* @return the filter
*/
private static DirectoryStream.Filter<Path> getDirsEqualsUuidFilter(final String uuid) {
return new DirectoryStream.Filter<Path>() {
@Override
public boolean accept(Path entry) throws IOException {
return Files.isDirectory(entry) && entry.getFileName().toString().equals(uuid);
}
};
};
/**
* Returns the temporary storage directory for an organization.
*
* @param org
* The organization
* @return Path to the temporary storage directory
*/
private Path getTemporaryStorageDir(final String org) {
return Paths.get(rootDirPath, org, "temp");
}
private Path getDurableStorageDir(final String org) {
return Paths.get(rootDirPath, org);
}
private Path getFile(final String org, final String uuid) throws NotFoundException, IOException {
// First check if the file is part of the durable storage section
try (DirectoryStream<Path> dirs = Files
.newDirectoryStream(getDurableStorageDir(org), getDirsEqualsUuidFilter(uuid))) {
for (Path dir : dirs) {
try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
for (Path file : files) {
return file;
}
}
}
}
// Second check if the file is part of the temporary storage section
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(getTemporaryStorageDir(org),
getDirsEqualsUuidFilter(uuid))) {
for (Path dir : dirs) {
try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
for (Path file : files) {
return file;
}
}
}
}
throw new NotFoundException(format("No file with UUID '%s' found.", uuid));
}
/**
* Deletes all files found in the temporary storage section of an organization.
*
* @param org
* The organization identifier
* @throws IOException
* if there was an error while deleting the files.
*/
void purgeTemporaryStorageSection(final String org, final long lifetime) throws IOException {
logger.info("Purge temporary storage section of organization '{}'", org);
final Path temporaryStorageDir = getTemporaryStorageDir(org);
if (Files.exists(temporaryStorageDir)) {
try (DirectoryStream<Path> tempFilesStream = Files.newDirectoryStream(temporaryStorageDir,
new DirectoryStream.Filter<Path>() {
@Override
public boolean accept(Path path) throws IOException {
return (Files.getLastModifiedTime(path).toMillis() < (new Date()).getTime() - lifetime);
}
})) {
for (Path file : tempFilesStream) {
FileUtils.deleteQuietly(file.toFile());
}
}
}
}
/**
* Deletes all files found in the temporary storage section of all known organizations.
*
* @throws IOException
* if there was an error while deleting the files.
*/
void purgeTemporaryStorageSection() throws IOException {
logger.info("Start purging temporary storage section of all known organizations");
for (Organization org : orgDirectory.getOrganizations()) {
purgeTemporaryStorageSection(org.getId(), TimeUnit.DAYS.toMillis(1));
}
}
/** Scheduled service for purging temporary storage sections. */
private class PurgeTemporaryStorageService extends AbstractScheduledService {
@Override
protected void runOneIteration() throws Exception {
StaticFileServiceImpl.this.purgeTemporaryStorageSection();
}
@Override
protected Scheduler scheduler() {
return Scheduler.newFixedRateSchedule(0, 1, TimeUnit.HOURS);
}
}
}