RepositoryServiceImpl.java
/*
* Copyright 2020 Global Crop Diversity Trust
*
* Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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.genesys.filerepository.service.impl;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import javax.persistence.EntityManager;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.tika.Tika;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.blocks.security.model.AclAwareModel;
import org.genesys.blocks.security.model.AclObjectIdentity;
import org.genesys.blocks.security.persistence.AclObjectIdentityPersistence;
import org.genesys.blocks.security.service.CustomAclService;
import org.genesys.filerepository.FolderNotEmptyException;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.NoSuchRepositoryFileException;
import org.genesys.filerepository.NoSuchRepositoryFolderException;
import org.genesys.filerepository.ReferencedRepositoryFileException;
import org.genesys.filerepository.ThumbnailException;
import org.genesys.filerepository.model.ImageGallery;
import org.genesys.filerepository.model.QRepositoryFile;
import org.genesys.filerepository.model.QRepositoryFolder;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.model.RepositoryImage;
import org.genesys.filerepository.persistence.ImageGalleryPersistence;
import org.genesys.filerepository.persistence.RepositoryFilePersistence;
import org.genesys.filerepository.persistence.RepositoryFolderRepository;
import org.genesys.filerepository.persistence.RepositoryImagePersistence;
import org.genesys.filerepository.service.BytesStorageService;
import org.genesys.filerepository.service.RepositoryService;
import org.genesys.filerepository.service.ThumbnailGenerator;
import org.genesys.filerepository.service.VirusFoundException;
import org.genesys.filerepository.service.VirusScanner;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.task.TaskExecutor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.querydsl.core.types.dsl.BooleanExpression;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
/**
* The RepositoryServiceImpl implementation.
*
* @author Matija Obreza
*/
@Service
@Slf4j
public class RepositoryServiceImpl implements RepositoryService, InitializingBean {
@Autowired
private EntityManager entityManager;
@Autowired
private JdbcTemplate jdbcTemplate;
/** The folder repository. */
@Autowired
private RepositoryFolderRepository folderRepository;
/** The repository file persistence. */
@Autowired
private RepositoryFilePersistence repositoryFilePersistence;
/** The repository image persistence. */
@Autowired
private RepositoryImagePersistence repositoryImagePersistence;
/** The image gallery persistence. */
@Autowired
private ImageGalleryPersistence imageGalleryPersistence;
/** The bytes storage service. */
@Autowired
private BytesStorageService bytesStorageService;
/** Thumbnail generator. */
@Autowired
private ThumbnailGenerator thumbnailGenerator;
/** The ACL service */
@Autowired
private CustomAclService aclService;
@Autowired(required = false)
private TaskExecutor taskExecutor;
@Autowired
private AclObjectIdentityPersistence aclObjectIdentityPersistence;
/** The blacklist file names regexp */
@Value("${repository.blacklist.filename}")
private String blacklistFileNameRegex;
/** The blacklist folder names regexp */
@Value("${repository.blacklist.foldername}")
private String blacklistFolderNameRegex;
/** The pattern of blacklist file names */
private Pattern blacklistFileNamePattern;
/** The pattern of blacklist folder names */
private Pattern blacklistFolderNamePattern;
private int[] thumbnailSizes = { 200 };
@Autowired(required = false)
private VirusScanner virusScanner;
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
blacklistFileNamePattern = Pattern.compile(blacklistFileNameRegex, Pattern.CASE_INSENSITIVE);
blacklistFolderNamePattern = Pattern.compile(blacklistFolderNameRegex, Pattern.CASE_INSENSITIVE);
}
private <T extends RepositoryFile> T lazyLoad(T repositoryFile) {
if (repositoryFile != null) {
repositoryFile.getFolder().getId();
}
return repositoryFile;
}
/**
* Just in case.
*/
@Transactional
public void addMissingHashSums() {
QRepositoryFile qRepositoryFile = QRepositoryFile.repositoryFile;
var filesMissingHashSums = this.repositoryFilePersistence
.findAll(qRepositoryFile.sha1Sum.isNull().or(qRepositoryFile.md5Sum.isNull()).or(qRepositoryFile.sha384.isNull()));
for (final RepositoryFile repositoryFile : filesMissingHashSums) {
log.debug("Updating SHA-1 and MD5 for file uuid={}", repositoryFile.getUuid());
try {
if (repositoryFile.getSize() > 5 * 1000 * 2000) {
log.info("File {} has size={}. Using a temporary file to calculate digests.", repositoryFile.getFilename(), repositoryFile.getSize());
bytesStorageService.get(repositoryFile.storagePath(), stream -> {
File tempFile = null;
try {
tempFile = File.createTempFile("repo", ".bin"); // Create a temporary file
try (OutputStream tempFileStream = new BufferedOutputStream(
Files.newOutputStream(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING), 8 * 1000)
) {
log.debug("Using temporary file {}", tempFile.getAbsolutePath());
stream.transferTo(tempFileStream);
}
updateDigests(repositoryFile, tempFile);
if (tempFile.exists()) {
tryDelete(tempFile);
}
} catch (IOException e) {
log.warn("Failed to generate hash sums: {}", e.getMessage());
} finally {
if (tempFile != null && tempFile.exists()) {
tryDelete(tempFile);
}
}
});
} else {
// Handle files < 5Mb in memory
final byte[] bytes = getFileBytes(repositoryFile);
repositoryFile.setSha1Sum(DigestUtils.sha1Hex(bytes));
repositoryFile.setSha384(DigestUtils.sha384(bytes));
repositoryFile.setMd5Sum(DigestUtils.md5Hex(bytes));
}
repositoryFilePersistence.save(repositoryFile);
} catch (final IOException e) {
log.warn("Failed to generate hash sums: {}", e.getMessage());
}
}
}
/**
* {@inheritDoc}
* Tranfer stream to a temporary file and upload that.
*/
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("isAuthenticated()")
public <T extends RepositoryFile> T addFile(Path repositoryPath, String originalFilename, String contentType, InputStream inputStream, T metaData) throws InvalidRepositoryPathException, InvalidRepositoryFileDataException, IOException {
if (inputStream == null) {
throw new InvalidRepositoryFileDataException("No bytes provided");
}
File tempFile = File.createTempFile("repo-", ".upload");
try {
try (OutputStream temp = new BufferedOutputStream(Files.newOutputStream(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING), 8 * 1000)) {
log.debug("Using temporary file {}", tempFile.getAbsolutePath());
inputStream.transferTo(temp);
}
return addFile(repositoryPath, originalFilename, contentType, tempFile, metaData);
} finally {
if (tempFile.exists()) {
tryDelete(tempFile);
}
}
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("isAuthenticated()")
public <T extends RepositoryFile> T addFile(final Path repositoryPath, String originalFilename, String contentType, final byte[] bytes, final T metaData)
throws InvalidRepositoryPathException, InvalidRepositoryFileDataException, IOException {
if (bytes == null) {
throw new InvalidRepositoryFileDataException("No bytes provided");
}
File tempFile = File.createTempFile("repo-", ".upload");
try {
try (OutputStream temp = new BufferedOutputStream(Files.newOutputStream(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING), 8 * 1000)) {
log.debug("Using temporary file {}", tempFile.getAbsolutePath());
temp.write(bytes);
}
return addFile(repositoryPath, originalFilename, contentType, tempFile, metaData);
} finally {
if (tempFile.exists()) {
tryDelete(tempFile);
}
}
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("isAuthenticated()")
public <T extends RepositoryFile> T addFile(final Path repositoryPath, String originalFilename, String contentType, final File fileWithData, final T metaData)
throws InvalidRepositoryPathException, InvalidRepositoryFileDataException, IOException {
if (fileWithData == null || !fileWithData.exists()) {
throw new InvalidRepositoryFileDataException("No bytes provided");
}
PathValidator.checkValidPath(repositoryPath);
RepositoryFolder repositoryFolder = ensureFolder(repositoryPath);
if (metaData != null) {
if (StringUtils.isNotBlank(metaData.getOriginalFilename())) {
originalFilename = metaData.getOriginalFilename();
}
if (StringUtils.isNotBlank(metaData.getContentType())) {
contentType = metaData.getContentType();
}
}
contentType = detectContentType(contentType, fileWithData);
// User can override the content type, so we can do it here too.
if (StringUtils.equals("text/plain", contentType)) {
if (originalFilename.endsWith(".css")) {
contentType = "text/css";
} else if (originalFilename.endsWith(".js")) {
contentType = "application/javascript";
}
}
if (contentType.isEmpty()) {
throw new InvalidRepositoryFileDataException("Content type not provided and could not be detected");
}
if ((originalFilename == null)) {
throw new InvalidRepositoryFileDataException();
}
if (blacklistFileNamePattern.matcher(originalFilename).find()) {
throw new InvalidRepositoryPathException("Invalid file name: " + originalFilename);
}
RepositoryFile repositoryFile = null;
if (metaData instanceof RepositoryImage || contentType.startsWith("image/")) {
log.trace("It's an image! contentType={}", contentType);
repositoryFile = new RepositoryImage();
} else {
repositoryFile = new RepositoryFile();
}
if (metaData != null) {
metaData.copyMetaData(repositoryFile);
}
updateDigests(repositoryFile, fileWithData);
repositoryFile.setSize((int) fileWithData.length());
repositoryFile.setFolder(repositoryFolder);
repositoryFile.setOriginalFilename(originalFilename);
repositoryFile.setContentType(contentType);
if (repositoryFile instanceof RepositoryImage) {
fillImageProperties((RepositoryImage) repositoryFile, fileWithData);
repositoryFile = repositoryImagePersistence.save((RepositoryImage) repositoryFile);
} else {
repositoryFile = repositoryFilePersistence.save(repositoryFile);
}
try {
bytesStorageService.upsert(repositoryFile.storagePath(), fileWithData);
} catch (final IOException e) {
log.debug("Failed to upload bytes", e);
throw e;
}
if (repositoryFile instanceof RepositoryImage) {
generateThumbnails((RepositoryImage) repositoryFile);
}
return (T) repositoryFile;
}
/**
* Update repositoryFile digests by streaming the file through DigestUtils
*/
private void updateDigests(RepositoryFile repositoryFile, File fileWithData) throws IOException {
// Calculate SHA-1 and MD5 sums
MessageDigest sha1 = DigestUtils.getSha1Digest();
MessageDigest sha384 = DigestUtils.getSha384Digest();
MessageDigest md5= DigestUtils.getMd5Digest();
byte[] buff = new byte[8*1000];
try (InputStream tempFileStream = new BufferedInputStream(Files.newInputStream(fileWithData.toPath()), 8 * 1000)) {
for (int len = tempFileStream.read(buff); len > 0; len = tempFileStream.read(buff)) {
sha1.update(buff, 0, len);
sha384.update(buff, 0, len);
md5.update(buff, 0, len);
}
repositoryFile.setSha1Sum(Hex.encodeHexString(sha1.digest()));
repositoryFile.setSha384(sha384.digest());
repositoryFile.setMd5Sum(Hex.encodeHexString(md5.digest()));
}
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("isAuthenticated()")
public RepositoryImage addImage(final Path repositoryPath, String originalFilename, String contentType, final byte[] bytes, final RepositoryImage metaData)
throws InvalidRepositoryPathException, InvalidRepositoryFileDataException, IOException {
return addFile(repositoryPath, originalFilename, contentType, bytes, metaData);
}
/**
* Ensure thumbnails exist for the {@link RepositoryImage}
*
* @param repositoryImage the repository image
*/
@Override
public void ensureThumbnails(final RepositoryImage repositoryImage) {
if (taskExecutor != null) {
taskExecutor.execute(() -> generateThumbnails(repositoryImage));
} else {
generateThumbnails(repositoryImage);
}
}
/**
* Ensure thumbnails of configured sizes for the repository image.
* Thumbnail images are generated in descending order by size (largest thumbnail
* first) and thumbnails of smaller sizes are generated from the previous
* thumbnail.
*
* @param repositoryImage the repository image
*/
private void generateThumbnails(final RepositoryImage repositoryImage) {
if (repositoryImage.getSize() > 10 * 1000 * 1000) {
log.info("Not generating thumbnails for large image {} with size {}b", repositoryImage.getOriginalFilename(), repositoryImage.getSize());
return;
}
try {
byte[][] webpCache = new byte[thumbnailSizes.length][];
for (int i = thumbnailSizes.length - 1; i >= 0; i--) {
final int cachePos = thumbnailSizes.length - i - 1;
log.debug("Making webp thumbnail #{} size={} cachePos={}", i, thumbnailSizes[i], cachePos);
webpCache[cachePos] = ensureThumbnail(thumbnailSizes[i], thumbnailSizes[i], RepositoryService.THUMB_EXT_WEBP, repositoryImage, () -> {
if (cachePos > 0 && webpCache[cachePos - 1] != null) {
log.debug("Using cached image bytes for {} at {}.", repositoryImage.getStoragePath(), cachePos - 1);
return webpCache[cachePos - 1];
} else {
log.debug("Must load image bytes for {}. Nothing cached yet.", repositoryImage.getStoragePath());
return bytesStorageService.get(repositoryImage.storagePath());
}
});
}
final byte[][] jpgCache = new byte[thumbnailSizes.length][];
for (int i = thumbnailSizes.length - 1; i >= 0; i--) {
final int cachePos = thumbnailSizes.length - i - 1;
log.debug("Making jpg thumbnail #{} size={} cachePos={}", i, thumbnailSizes[i], cachePos);
jpgCache[cachePos] = ensureThumbnail(thumbnailSizes[i], thumbnailSizes[i], RepositoryService.THUMB_EXT_JPG, repositoryImage, () -> {
if (cachePos > 0 && jpgCache[cachePos - 1] != null) {
log.debug("Using cached image bytes for {} at {}.", repositoryImage.getStoragePath(), cachePos - 1);
return jpgCache[cachePos - 1];
} else {
log.debug("Must load image bytes for {}. Nothing cached yet.", repositoryImage.getStoragePath());
return bytesStorageService.get(repositoryImage.storagePath());
}
});
}
} catch (final Exception e) {
log.error("Error generating thumbnail for {}: {}", repositoryImage, e.getMessage(), e);
}
}
/**
* Ensure thumbnail.
*
* @param width the width
* @param height the height
* @param repositoryImage the repository image
*
* @throws IOException Signals that an I/O exception has occurred.
* @throws InvalidRepositoryPathException if path is messed up
*/
private byte[] ensureThumbnail(final Integer width, final Integer height, final String extension, final RepositoryImage repositoryImage, final IImageBytesProvider loader) throws IOException, InvalidRepositoryPathException {
final String filename = getThumbnailFilename(width, height, extension);
if (!bytesStorageService.exists(getFullThumbnailsPath(repositoryImage).resolve(filename))) {
log.debug("Generating new thumbnail width={} height={} for image={}", width, height, repositoryImage.getUuid());
try {
final byte[] thumbnailBytes = thumbnailGenerator.createThumbnail(width, height, extension, loader.getImageBytes());
log.debug("Persisting new thumbnail width={} height={} for image={}", width, height, repositoryImage.getUuid());
bytesStorageService.upsert(getFullThumbnailsPath(repositoryImage).resolve(filename), thumbnailBytes);
return thumbnailBytes;
} catch (Throwable e) {
log.warn("Error generating thumbnail: {}", e.getMessage());
}
}
return null;
}
/**
* Path to /_thumbs/{UUID#short}/{UUID}.
*
* @param image the image
* @return the full thumbnails path
*/
public static Path getFullThumbnailsPath(final RepositoryImage image) {
return Paths.get(THUMB_PATH, image.getThumbnailPath());
}
/**
* Gets the thumbnail filename.
*
* @param width the width
* @param height the height
* @param extension the file extension of the thumbnail image
* @return the thumbnail filename
*/
public static String getThumbnailFilename(final Integer width, final Integer height, final String extension) {
final StringBuilder sb = new StringBuilder();
if (width != null) {
sb.append(width);
}
sb.append('x');
if (height != null) {
sb.append(height);
}
sb.append('.').append(extension);
return sb.toString();
}
/**
* Load image to get pixel width and height.
*
* @param repositoryImage the repository image
* @param bytes the bytes
* @throws IOException When things go wrong
*/
private void fillImageProperties(final RepositoryImage repositoryImage, final File file) {
if (file.exists() && file.length() < 10 * 1000 * 1000) {
try {
final BufferedImage imageData = ImageIO.read(file);
if (imageData != null) {
repositoryImage.setWidth(imageData.getWidth());
repositoryImage.setHeight(imageData.getHeight());
}
} catch (Throwable e) {
log.warn("Could not read image metadata: {}", e.getMessage());
repositoryImage.setWidth(0);
repositoryImage.setHeight(0);
}
} else {
try {
Metadata metadata = ImageMetadataReader.readMetadata(file);
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
repositoryImage.setWidth(directory.getInt(ExifSubIFDDirectory.TAG_IMAGE_WIDTH));
repositoryImage.setHeight(directory.getInt(ExifSubIFDDirectory.TAG_IMAGE_HEIGHT));
} catch (Throwable e) {
log.warn("Could not read metadata: {}", e.getMessage());
repositoryImage.setWidth(0);
repositoryImage.setHeight(0);
}
}
}
/*
* (non-Javadoc)
* @see org.genesys.filerepository.service.RepositoryService#getFile(java
* .util.UUID)
*/
@SuppressWarnings("unchecked")
@Override
@Transactional(readOnly = true)
@PostAuthorize("hasRole('ADMINISTRATOR') or hasPermission(returnObject, 'read')")
public <T extends RepositoryFile> T getFile(final UUID fileUuid) throws NoSuchRepositoryFileException {
RepositoryFile file = repositoryFilePersistence.findByUuid(fileUuid);
if (file != null) {
return lazyLoad((T) file);
} else {
throw new NoSuchRepositoryFileException("No such file in repository");
}
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#getFile(java.util.UUID,
* int)
*/
@SuppressWarnings("unchecked")
@Override
@Transactional(readOnly = true)
@PostAuthorize("hasRole('ADMINISTRATOR') or hasPermission(returnObject, 'read')")
public <T extends RepositoryFile> T getFile(UUID fileUuid, int version) throws NoSuchRepositoryFileException {
RepositoryFile file = repositoryFilePersistence.findByUuidAndVersion(fileUuid, version);
if (file != null) {
return (T) file;
} else {
throw new NoSuchRepositoryFileException("No such file in repository");
}
}
/*
* (non-Javadoc)
* @see org.genesys.filerepository.service.RepositoryService#getFile(java.lang.
* String, java.lang.String)
*/
@SuppressWarnings("unchecked")
@Override
@Transactional(readOnly = true)
@PostAuthorize("hasRole('ADMINISTRATOR') or hasPermission(returnObject, 'read')")
public <T extends RepositoryFile> T getFile(final Path path, final String originalFilename) throws NoSuchRepositoryFileException, InvalidRepositoryPathException {
PathValidator.checkValidPath(path);
RepositoryFolder folder = folderRepository.findByPath(path.toString());
if (folder == null) {
throw new NoSuchRepositoryFileException("No file at path=" + path + " originalFilename=" + originalFilename);
}
final RepositoryFile repositoryFile = repositoryFilePersistence.findByFolderAndOriginalFilename(folder, originalFilename);
if (repositoryFile == null) {
throw new NoSuchRepositoryFileException("No file at path=" + path + " originalFilename=" + originalFilename);
}
return (T) repositoryFile;
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#getFileBytes(org.genesys
* .filerepository.model.RepositoryFile)
*/
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'read')")
public byte[] getFileBytes(final RepositoryFile repositoryFile) throws IOException {
if (repositoryFile.getStoragePath() == null) return null;
return bytesStorageService.get(repositoryFile.storagePath());
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'read')")
public void streamFileBytes(RepositoryFile repositoryFile, OutputStream outputStream) throws IOException {
final var th = new ThrowableHolder<IOException>();
bytesStorageService.get(repositoryFile.storagePath(), (inputStream) -> {
try (inputStream) {
inputStream.transferTo(outputStream);
} catch (IOException e) {
th.throwable = e;
}
});
if (th.throwable != null) {
log.warn("Streaming bytes from storage threw {}", th.throwable, th.throwable);
throw th.throwable;
}
}
@Override
@Transactional(readOnly = true)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFolder, 'read')")
public OutputStream getFolderAsZip(RepositoryFolder repositoryFolder, OutputStream outputStream, int maxFilesLimit) throws InvalidRepositoryPathException, IOException{
var folders = getAllSubFolders(repositoryFolder, new ArrayList<>());
folders.add(0, repositoryFolder);
log.info("Found {} folders including {}", folders.size(), repositoryFolder.getFolderPath());
var fileCounter = repositoryFilePersistence.count(QRepositoryFile.repositoryFile.folder.in(folders));
log.warn("Folder {} has {} files to download", repositoryFolder.getPath(), fileCounter);
if (fileCounter >= maxFilesLimit) {
throw new IOException("File count exceeds ZIP file limit of " + maxFilesLimit);
}
return getFolderAsZip(repositoryFolder, outputStream);
}
@Override
@Transactional(readOnly = true)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFolder, 'read')")
public OutputStream getFolderAsZip(RepositoryFolder repositoryFolder, OutputStream outputStream) throws InvalidRepositoryPathException, IOException{
log.warn("Downloading {} folder as zip archive", repositoryFolder.getPath());
final ZipOutputStream zos = new ZipOutputStream(outputStream);
writeToZip(zos, repositoryFolder, Paths.get(""));
zos.finish();
zos.flush();
return zos;
}
private List<RepositoryFolder> getAllSubFolders(RepositoryFolder root, List<RepositoryFolder> folders) throws InvalidRepositoryPathException {
var subFolders = getFolders(root.getFolderPath(), Sort.unsorted());
folders.addAll(subFolders);
for (var subFolder : subFolders) {
getAllSubFolders(subFolder, folders);
}
return folders;
}
private void writeToZip(final ZipOutputStream zos, RepositoryFolder folder, Path zipPath) throws InvalidRepositoryPathException, IOException {
List<RepositoryFile> files = getFiles(folder.getFolderPath(), Sort.unsorted());
log.info("Writing {} files from folder {}", files.size(), folder.getPath());
for (RepositoryFile file : files) {
final ZipEntry fileEntry = new ZipEntry(zipPath.resolve(file.getOriginalFilename()).toString());
fileEntry.setComment(file.getDescription());
fileEntry.setTime(System.currentTimeMillis());
zos.putNextEntry(fileEntry);
try (var proxy = new OutputStream() {
@Override
public void write(int b) throws IOException {
zos.write(b);
}
public void write(byte[] b) throws IOException {
zos.write(b);
};
public void write(byte[] b, int off, int len) throws IOException {
zos.write(b, off, len);
zos.flush();
};
public void close() throws IOException {
zos.flush();
};
}) {
// Stream repository bytes to proxy which writes them directly to ZipOutputStream
streamFileBytes(file, proxy);
}
zos.flush();
zos.closeEntry();
}
List<RepositoryFolder> subFolders = getFolders(folder.getFolderPath(), Sort.unsorted());
for (RepositoryFolder subFolder: subFolders) {
final ZipEntry folderEntry = new ZipEntry(zipPath.resolve(subFolder.getName()).toString().concat("/"));
folderEntry.setComment(subFolder.getDescription());
folderEntry.setTime(System.currentTimeMillis());
zos.putNextEntry(folderEntry);
zos.flush();
zos.closeEntry();
writeToZip(zos, subFolder, zipPath.resolve(subFolder.getName()));
}
}
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'write')")
@Transactional(rollbackFor = Throwable.class)
public List<RepositoryFile> extractZip(final RepositoryFile repositoryFile) throws NoSuchRepositoryFileException, InvalidRepositoryFileDataException, IOException {
assert (repositoryFile != null && repositoryFile.getId() != null);
RepositoryFile file = repositoryFilePersistence.findById(repositoryFile.getId()).orElseThrow(() -> new NoSuchRepositoryFileException());
if (! StringUtils.equalsIgnoreCase(file.getExtension(), ".zip")) {
throw new InvalidRepositoryFileDataException("File extension must be .zip for extracting: ".concat(repositoryFile.getOriginalFilename()));
}
byte[] fileBytes = getFileBytes(file);
Path repositoryFilePath = Paths.get(file.getFolder().getPath());
try (final ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(fileBytes))) {
List<RepositoryFile> savedFiles = new ArrayList<>();
var zipEntry = zip.getNextEntry();
while (zipEntry != null) {
if (zipEntry.isDirectory()) {
try {
ensureFolder(repositoryFilePath.resolve(zipEntry.getName()));
} catch (InvalidRepositoryPathException e) {
log.warn("Ignoring invalid path in archive: {}", e.getMessage());
}
} else {
Path zipFilePath = Paths.get(zipEntry.getName());
Path repositoryPath = repositoryFilePath;
if (zipFilePath.getParent() != null) {
repositoryPath = repositoryFilePath.resolve(zipFilePath.getParent());
}
String contentType = URLConnection.getFileNameMap().getContentTypeFor(zipFilePath.getFileName().toString());
try {
var savedFile = addFile(repositoryPath, zipFilePath.getFileName().toString(), contentType, zip.readAllBytes(), null);
savedFiles.add(savedFile);
} catch (InvalidRepositoryPathException e) {
log.warn("Ignoring invalid path in archive: {}", e.getMessage());
}
}
zipEntry = zip.getNextEntry();
}
return savedFiles;
} catch (IOException e) {
log.error("Could not extract zip", e);
throw new InvalidRepositoryFileDataException("Could not extract data from zip archive: ".concat(e.getMessage()), e);
}
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'read')")
public <T extends RepositoryFile> void scanBytes(T repositoryFile) throws VirusFoundException, IOException {
if (virusScanner != null) {
var throwableHolder = new ThrowableHolder<VirusFoundException>();
bytesStorageService.get(repositoryFile.storagePath(), inputStream -> {
try {
virusScanner.scan(inputStream);
} catch (VirusFoundException e) {
throwableHolder.throwable = e;
log.warn("Virus found in repository file uuid={} path={}/{}", repositoryFile.getUuid(), repositoryFile.getFolder().getPath(), repositoryFile.getOriginalFilename());
}
});
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
}
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#getFiles(java.nio.file.
* Path)
*/
@Override
@Transactional(readOnly = true)
public List<RepositoryFile> getFiles(Path folderPath, final Sort sort) throws InvalidRepositoryPathException {
RepositoryFolder folder = getFolder(folderPath);
if (folder == null) {
return Collections.emptyList();
}
return (List<RepositoryFile>) repositoryFilePersistence.findAll(QRepositoryFile.repositoryFile.folder.eq(folder), sort);
}
/* (non-Javadoc)
* @see org.genesys.filerepository.service.RepositoryService#listFiles(java.nio.file.Path, org.springframework.data.domain.Pageable)
*/
@Override
@Transactional(readOnly = true)
public Page<RepositoryFile> listFiles(Path folderPath, Pageable page) throws InvalidRepositoryPathException {
RepositoryFolder folder = getFolder(folderPath);
if (folder == null) {
return new PageImpl<>(Collections.emptyList(), page, 0);
}
return repositoryFilePersistence.findAll(QRepositoryFile.repositoryFile.folder.eq(folder), page);
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#getFolders(java.nio.file
* .Path)
*/
@Override
@Transactional(readOnly = true)
public List<RepositoryFolder> getFolders(Path root, final Sort sort) throws InvalidRepositoryPathException {
PathValidator.checkValidPath(root);
if (root == null || "/".equals(root.normalize().toAbsolutePath().toString())) {
return (List<RepositoryFolder>) folderRepository.findAll(QRepositoryFolder.repositoryFolder.parent.isNull(), sort);
} else {
RepositoryFolder parent = getFolder(root);
return parent == null ? Collections.emptyList() :
// Load child folders
(List<RepositoryFolder>) folderRepository.findAll(QRepositoryFolder.repositoryFolder.parent.eq(parent), sort);
}
}
/* (non-Javadoc)
* @see org.genesys.filerepository.service.RepositoryService#listFolders(java.nio.file.Path, org.springframework.data.domain.Pageable)
*/
@Override
@Transactional(readOnly = true)
public Page<RepositoryFolder> listFolders(Path root, Pageable page) throws InvalidRepositoryPathException {
PathValidator.checkValidPath(root);
if ("/".equals(root.normalize().toAbsolutePath().toString())) {
return folderRepository.findAll(QRepositoryFolder.repositoryFolder.parent.isNull(), page);
} else {
RepositoryFolder parent = getFolder(root);
return parent == null ? new PageImpl<>(Collections.emptyList(), page, 0) :
// Load child folders
folderRepository.findAll(QRepositoryFolder.repositoryFolder.parent.eq(parent), page);
}
}
@Override
@Transactional(readOnly = true)
public RepositoryFolder getFolder(UUID uuid) {
return folderRepository.findByUuid(uuid);
}
@Override
@Transactional(readOnly = true)
public RepositoryFolder getFolder(Path folderPath) throws InvalidRepositoryPathException {
PathValidator.checkValidPath(folderPath);
return folderRepository.findByPath(folderPath.normalize().toAbsolutePath().toString());
}
/* (non-Javadoc)
* @see org.genesys.filerepository.service.RepositoryService#updateFolder(org.genesys.filerepository.model.RepositoryFolder)
*/
@Override
@Transactional(rollbackFor = Throwable.class)
public RepositoryFolder updateFolder(RepositoryFolder folder) throws NoSuchRepositoryFolderException {
RepositoryFolder databaseFolder = folderRepository.findByUuidAndVersion(folder.getUuid(), folder.getVersion());
if (databaseFolder == null) {
throw new NoSuchRepositoryFolderException("Folder version doesn't match or folder not found");
}
databaseFolder.setTitle(folder.getTitle());
databaseFolder.setDescription(folder.getDescription());
return folderRepository.save(databaseFolder);
}
/** {@inheritDoc} */
@Override
@Transactional(readOnly = true)
public Stream<RepositoryFile> streamFiles(Path root, final Sort sort) throws InvalidRepositoryPathException {
PathValidator.checkValidPath(root);
BooleanExpression q = QRepositoryFile.repositoryFile.folder.in(listPathsRecursively(root));
return StreamSupport.stream(repositoryFilePersistence.findAll(q, sort).spliterator(), false);
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'write')")
public <T extends RepositoryFile> T updateMetadata(final T repositoryFile) throws NoSuchRepositoryFileException {
RepositoryFile rf = repositoryFilePersistence.findByUuidAndVersion(repositoryFile.getUuid(), repositoryFile.getVersion());
if (rf == null) {
throw new NoSuchRepositoryFileException();
}
rf.apply(repositoryFile);
if (rf instanceof RepositoryImage) {
return (T) lazyLoad(repositoryImagePersistence.save((RepositoryImage) rf));
} else {
return (T) lazyLoad(repositoryFilePersistence.save(rf));
}
}
@Override
@Transactional
public Map<String, String> updateMetadata(final List<RepositoryFile> updates) {
boolean hasRoleAdministrator = SecurityContextUtil.hasRole("ADMINISTRATOR");
var updateUuids = updates.stream().map(RepositoryFile::getUuid).collect(Collectors.toList());
List<RepositoryFile> rfs = repositoryFilePersistence.findByUuid(updateUuids);
var rfUuids = rfs.stream().map(RepositoryFile::getUuid).collect(Collectors.toSet());
var errors = new LinkedHashMap<String, String>();
for (var updateUuid : updateUuids) {
if (! rfUuids.contains(updateUuid)) {
errors.put(updateUuid.toString(), "No such file " + updateUuid);
}
}
for (var rf : rfs) {
var update = updates.stream().filter(f -> Objects.equals(f.getUuid(), rf.getUuid())).findFirst().orElse(null);
if (update == null) {
errors.put(rf.getUuid().toString(), "Could not find update for file " + rf.getUuid());
continue;
}
boolean changed = false;
if (! StringUtils.equals(rf.getTitle(), update.getTitle())) {
rf.setTitle(update.getTitle());
changed = true;
}
if (! StringUtils.equals(rf.getSubject(), update.getSubject())) {
rf.setSubject(update.getSubject());
changed = true;
}
if (! StringUtils.equals(rf.getDescription(), update.getDescription())) {
rf.setDescription(update.getDescription());
changed = true;
}
if (! StringUtils.equals(rf.getCreator(), update.getCreator())) {
rf.setCreator(update.getCreator());
changed = true;
}
if (! StringUtils.equals(rf.getCreated(), update.getCreated())) {
rf.setCreated(update.getCreated());
changed = true;
}
if (! StringUtils.equals(rf.getRightsHolder(), update.getRightsHolder())) {
rf.setRightsHolder(update.getRightsHolder());
changed = true;
}
if (! StringUtils.equals(rf.getAccessRights(), update.getAccessRights())) {
rf.setAccessRights(update.getAccessRights());
changed = true;
}
if (! StringUtils.equals(rf.getLicense(), update.getLicense())) {
rf.setLicense(update.getLicense());
changed = true;
}
if (! StringUtils.equals(rf.getBibliographicCitation(), update.getBibliographicCitation())) {
rf.setBibliographicCitation(update.getBibliographicCitation());
changed = true;
}
if (changed) {
if (! hasRoleAdministrator && ! SecurityContextUtil.hasPermission(rf, "WRITE")) {
errors.put(rf.getUuid().toString(), "No WRITE permission on " + rf.getUuid());
continue;
}
entityManager.persist(rf);
}
}
return errors;
}
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryImage, 'write')")
public RepositoryImage updateImageMetadata(RepositoryImage repositoryImage) throws NoSuchRepositoryFileException {
return updateMetadata(repositoryImage);
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'write')")
public <T extends RepositoryFile> T updateBytes(T repositoryFile, String contentType, InputStream inputStream)
throws NoSuchRepositoryFileException, IOException {
File tempFile = File.createTempFile("repo-", ".upload");
try {
try (OutputStream temp = new BufferedOutputStream(Files.newOutputStream(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING), 8 * 1000)) {
log.debug("Using temporary file {}", tempFile.getAbsolutePath());
inputStream.transferTo(temp);
}
return updateBytes(repositoryFile, contentType, tempFile);
} finally {
if (tempFile.exists()) {
tryDelete(tempFile);
}
}
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'write')")
public <T extends RepositoryFile> T updateBytes(final T repositoryFile, String contentType, final byte[] bytes) throws NoSuchRepositoryFileException, IOException {
File tempFile = File.createTempFile("repo-", ".upload");
try {
try (OutputStream temp = new BufferedOutputStream(Files.newOutputStream(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING), 8 * 1000)) {
log.debug("Using temporary file {}", tempFile.getAbsolutePath());
temp.write(bytes);
}
return updateBytes(repositoryFile, contentType, tempFile);
} finally {
if (tempFile.exists()) {
tryDelete(tempFile);
}
}
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryImage, 'write')")
public RepositoryImage updateImageBytes(RepositoryImage repositoryImage, String contentType, byte[] bytes)
throws NoSuchRepositoryFileException, IOException {
return updateBytes(repositoryImage, contentType, bytes);
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'write')")
public <T extends RepositoryFile> T updateBytes(T repositoryFile, String contentType, File fileWithBytes)
throws NoSuchRepositoryFileException, IOException {
T storedFile = getFile(repositoryFile.getUuid(), repositoryFile.getVersion());
// Update metadata
storedFile.apply(repositoryFile);
contentType = detectContentType(contentType, fileWithBytes);
storedFile.setContentType(contentType);
// Calculate SHA-1 and MD5 sums
log.debug("updateBytes length={}", fileWithBytes.length());
updateDigests(storedFile, fileWithBytes);
var fileLength = fileWithBytes.length();
storedFile.setSize((int) fileLength);
log.debug("updateBytes length={} repoFile.size={}", fileLength, storedFile.getSize());
if (storedFile instanceof RepositoryImage) {
fillImageProperties((RepositoryImage) storedFile, fileWithBytes);
}
bytesStorageService.upsert(storedFile.storagePath(), fileWithBytes);
if (storedFile instanceof RepositoryImage) {
return (T) lazyLoad(repositoryFilePersistence.save((RepositoryImage) storedFile));
} else {
return lazyLoad(repositoryFilePersistence.save(storedFile));
}
}
/**
* Update content type if necessary.
*
* @param contentType the content type
* @param bytes the bytes
* @param originalFilename
* @return the string
*/
private String detectContentType(final String contentType, @NonNull final File file) {
if (!file.exists()) {
return contentType;
}
try {
if (contentType == null) {
return new Tika().detect(file);
} else {
final String detectedContentType = new Tika().detect(file);
if (! detectedContentType.equals(contentType)) {
log.debug("Content-Type provided={} detected={}", contentType, detectedContentType);
}
return contentType;
}
} catch (IOException e) {
log.info("Could not detect content type: {}", e.getMessage());
return contentType;
}
}
/** {@inheritDoc} */
@Override
@Transactional(propagation = Propagation.MANDATORY, noRollbackFor = { ReferencedRepositoryFileException.class, NoSuchRepositoryFileException.class }, rollbackFor = IOException.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'delete')")
public <T extends RepositoryFile> T removeFileIfPossible(T repositoryFile) throws ReferencedRepositoryFileException, NoSuchRepositoryFileException, IOException {
if (repositoryFile == null) {
throw new NoSuchRepositoryFileException();
}
// ensure repositoryFile isn't proxy
if (repositoryFile instanceof HibernateProxy) {
repositoryFile = (T) Hibernate.unproxy(repositoryFile);
}
// Try to delete the record
if (repositoryFile instanceof RepositoryImage) {
try {
jdbcTemplate.execute("delete from repository_image where id = " + repositoryFile.getId());
jdbcTemplate.execute("delete from repository_file where id = " + repositoryFile.getId());
log.debug("repository_image and repository_file removed!");
} catch (DataIntegrityViolationException e) {
log.debug("Err: {}", e.getMessage(), e);
throw new ReferencedRepositoryFileException("Could not delete repositoryFile", e);
}
} else {
try {
jdbcTemplate.execute("delete from repository_file where id = " + repositoryFile.getId());
log.debug("repository_file removed!");
} catch (DataIntegrityViolationException e) {
log.debug("Err: {}", e.getMessage(), e);
throw new ReferencedRepositoryFileException("Could not delete repositoryFile", e);
}
}
if (repositoryFile instanceof RepositoryImage) {
_removeImage((RepositoryImage) repositoryFile);
}
// Delete the bytes
bytesStorageService.remove(repositoryFile.storagePath());
return repositoryFile;
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'delete')")
public <T extends RepositoryFile> T removeFile(T repositoryFile) throws NoSuchRepositoryFileException, IOException {
if (repositoryFile == null) {
throw new NoSuchRepositoryFileException();
}
// ensure repositoryFile isn't proxy
if (repositoryFile instanceof HibernateProxy) {
repositoryFile = (T) Hibernate.unproxy(repositoryFile);
}
// Try to delete the record
if (repositoryFile instanceof RepositoryImage) {
repositoryImagePersistence.delete((RepositoryImage) repositoryFile);
} else {
repositoryFilePersistence.delete(repositoryFile);
}
if (repositoryFile instanceof RepositoryImage) {
_removeImage((RepositoryImage) repositoryFile);
}
// Delete the bytes
bytesStorageService.remove(repositoryFile.storagePath());
return repositoryFile;
}
private void _removeImage(final RepositoryImage repositoryImage) throws NoSuchRepositoryFileException, IOException {
// Remove thumbnails
Path path = getFullThumbnailsPath(repositoryImage);
try {
List<String> thumbnails = bytesStorageService.listFiles(path);
for (String thumb : thumbnails) {
log.debug("Removing thumbnail at {}", path.resolve(thumb));
try {
bytesStorageService.remove(path.resolve(Path.of(thumb).getFileName()));
} catch (IOException e) {
// Log error on thumbnails, don't throw exception
log.warn("Trouble removing thumbnail: {}", e.getMessage());
}
}
} catch (Exception e) {
log.warn("Trouble removing thumbnails: {}", e.getMessage());
}
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryImage, 'delete')")
public RepositoryImage removeImage(RepositoryImage repositoryImage) throws NoSuchRepositoryFileException, IOException {
return removeFile(repositoryImage);
}
/*
* (non-Javadoc)
* @see org.genesys.filerepository.service.RepositoryService#moveFile(org
* .genesys2.server.filerepository.model .RepositoryFile, java.lang.String)
*/
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'write')")
public <T extends RepositoryFile> T moveFile(final T repositoryFile, final Path destination) throws NoSuchRepositoryFileException, InvalidRepositoryPathException {
if (repositoryFile == null) {
throw new NoSuchRepositoryFileException();
}
if (destination == null) {
throw new InvalidRepositoryPathException("destination cannot be null");
}
PathValidator.checkValidPath(destination);
RepositoryFolder repositoryFolder = ensureFolder(destination);
repositoryFile.setFolder(repositoryFolder);
if (repositoryFile instanceof RepositoryImage) {
return (T) lazyLoad(repositoryImagePersistence.save((RepositoryImage) repositoryFile));
} else {
return (T) lazyLoad(repositoryFilePersistence.save(repositoryFile));
}
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#moveAndRenameFile(org.
* genesys.filerepository.model.RepositoryFile, java.lang.String)
*/
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#repositoryFile, 'write')")
public <T extends RepositoryFile> T moveAndRenameFile(T repositoryFile, final Path fullPath) throws InvalidRepositoryPathException, InvalidRepositoryFileDataException {
if (fullPath == null) {
throw new NullPointerException("Full path cannot be null");
}
// ensure repositoryFile isn't proxy
if (repositoryFile instanceof HibernateProxy) {
repositoryFile = (T) Hibernate.unproxy(repositoryFile);
}
log.info("Moving name={} to {}", repositoryFile.getFilename(), fullPath);
final Path path = fullPath.normalize().toAbsolutePath();
PathValidator.checkValidPath(path.getParent().toString());
if (path.getFileName().toString().trim().isEmpty()) {
throw new InvalidRepositoryFileDataException("File name cannot be blank");
}
repositoryFile.setFolder(ensureFolder(path.getParent()));
repositoryFile.setOriginalFilename(path.getFileName().toString());
if (repositoryFile instanceof RepositoryImage) {
return (T) lazyLoad(repositoryImagePersistence.save((RepositoryImage) repositoryFile));
} else {
return (T) lazyLoad(repositoryFilePersistence.save(repositoryFile));
}
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#listImages(java.lang.
* String)
*/
@Override
@Transactional(readOnly = true)
public List<RepositoryImage> listImages(final Path repositoryPath, final Sort sort) {
return repositoryImagePersistence.findByFolder(folderRepository.findByPath(repositoryPath.normalize().toAbsolutePath().toString()), sort);
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#hasPath(java.nio.file.
* Path)
*/
@Override
@Transactional(readOnly = true)
public boolean hasPath(final Path path) throws InvalidRepositoryPathException {
PathValidator.checkValidPath(path);
final String p = path.normalize().toAbsolutePath().toString();
PathValidator.checkValidPath(p);
log.trace("Is directory path={}", p);
return folderRepository.exists(QRepositoryFolder.repositoryFolder.path.eq(p));
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#listPaths(java.lang.
* String)
*/
@Override
@Transactional(readOnly = true)
public List<RepositoryFolder> listPathsRecursively(final Path prefix) throws InvalidRepositoryPathException {
PathValidator.checkValidPath(prefix);
ArrayList<RepositoryFolder> folders = new ArrayList<>();
RepositoryFolder rootFolder = folderRepository.findByPath(prefix.normalize().toAbsolutePath().toString());
if (rootFolder != null) {
folders.add(rootFolder);
addAllSubfolders(rootFolder, folders);
}
return folders;
}
/**
* Recursively Adds the all subfolders to the list
*
* @param folder the root folder
* @param folders the folders
*/
private void addAllSubfolders(RepositoryFolder folder, ArrayList<RepositoryFolder> folders) {
if (folder != null && folder.getChildren() != null) {
for (RepositoryFolder ch : folder.getChildren()) {
folders.add(ch);
addAllSubfolders(ch, folders);
}
}
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#renamePath(java.lang.
* String, java.lang.String)
*/
@Override
@Transactional(rollbackFor = Throwable.class)
public RepositoryFolder renamePath(final Path currentPath, final Path newPath) throws InvalidRepositoryPathException {
PathValidator.checkValidPath(currentPath);
PathValidator.checkValidPath(newPath);
final Path cPath = currentPath.normalize().toAbsolutePath();
final Path nPath = newPath.normalize().toAbsolutePath();
PathValidator.checkValidPath(cPath.toString());
PathValidator.checkValidPath(nPath.toString());
RepositoryFolder source = folderRepository.findByPath(cPath.toString());
if (source == null) {
throw new InvalidRepositoryPathException("Folder for rename doesn't exist. Path was " + cPath);
} else if (!SecurityContextUtil.hasRole("ADMINISTRATOR") && !SecurityContextUtil.hasPermission(source, "MANAGE")) {
throw new AccessDeniedException("No MANAGE permission on " + cPath);
}
RepositoryFolder target = folderRepository.findByPath(nPath.toString());
if (target != null) {
throw new InvalidRepositoryPathException("Folder with this name already exists. Path was " + nPath);
}
var reversedPaths = listPathsRecursively(cPath);
Collections.reverse(reversedPaths);
for (final RepositoryFolder folder : reversedPaths) {
final Path folderPath = folder.getFolderPath();
final Path relative = cPath.relativize(folderPath);
log.debug("Base={} rfPath={} relative={} to={}", cPath, folderPath, relative, nPath.resolve(relative));
final RepositoryFolder newFolder = ensureFolder(nPath.resolve(relative).toAbsolutePath());
copySecurityData(folder, newFolder);
for (final RepositoryFile repositoryFile : folder.getFiles()) {
repositoryFile.setFolder(newFolder);
repositoryFilePersistence.save(repositoryFile);
}
final ImageGallery imageGallery = folder.getGallery();
if (imageGallery != null) {
imageGallery.setFolder(newFolder);
imageGallery.setPath(newFolder.getPath());
imageGalleryPersistence.save(imageGallery);
}
log.debug("Deleting folder {}", folder);
folderRepository.delete(folder);
}
return folderRepository.findByPath(nPath.toString());
}
private void copySecurityData(AclAwareModel source, AclAwareModel target) {
AclObjectIdentity sourceAclObjectIdentity = aclService.getObjectIdentity(source);
AclObjectIdentity targetAclObjectIdentity = aclService.getObjectIdentity(target);
if (sourceAclObjectIdentity != null && targetAclObjectIdentity != null) {
targetAclObjectIdentity.setOwnerSid(sourceAclObjectIdentity.getOwnerSid());
aclObjectIdentityPersistence.save(targetAclObjectIdentity);
} else {
log.debug("Can't change owner source={}/{} target={}/{}", source, sourceAclObjectIdentity, target, targetAclObjectIdentity);
}
// Add permissions defined on source (if any) to target
aclService.getPermissions(source).forEach(sourceSidPermission -> {
log.debug("Copying permission {} to {}", sourceSidPermission, target);
aclService.setPermissions(target, sourceSidPermission.sid, sourceSidPermission);
});
}
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR')")
public RepositoryFolder ensureFolder(Path folderPath, AclAwareModel parentEntity) throws InvalidRepositoryPathException {
assert(folderPath != null);
assert(parentEntity != null);
PathValidator.checkValidPath(folderPath);
folderPath = folderPath.normalize().toAbsolutePath();
PathValidator.checkValidPath(folderPath.toString());
if ("/".equals(folderPath.toString())) {
// Root folder
if (! SecurityContextUtil.hasRole("ADMINISTRATOR")) {
// Only administrator can create folders on root
throw new AccessDeniedException("No WRITE permission on /");
}
return null;
} else {
String folderNameToCheck = folderPath.getFileName().toString();
if (blacklistFolderNamePattern.matcher(folderNameToCheck).find()) {
throw new InvalidRepositoryPathException("Invalid folder name: " + folderNameToCheck);
}
PathValidator.checkValidFolderName(folderNameToCheck);
}
RepositoryFolder folder = folderRepository.findByPath(folderPath.toString());
if (folder == null) {
// Create new folder
folder = new RepositoryFolder();
folder.setName(folderPath.getFileName().toString());
// This checks permissions on parent, but we're running as ADMIN
folder.setParent(ensureFolder(folderPath.getParent()));
folder.setParentOid(aclService.getObjectIdentity(parentEntity));
if (folder.getParentOid() == null) {
throw new RuntimeException("Parent entity does not have an ACL OID: " + parentEntity);
}
folder = folderRepository.save(folder);
} else {
// Update parent OID
folder.setParentOid(aclService.getObjectIdentity(parentEntity));
folder = folderRepository.save(folder);
}
return folder;
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#addFolder(java.nio.file.
* Path)
*/
@Override
@Transactional(rollbackFor = Throwable.class)
public RepositoryFolder ensureFolder(Path folderPath) throws InvalidRepositoryPathException {
PathValidator.checkValidPath(folderPath);
folderPath = folderPath.normalize().toAbsolutePath();
PathValidator.checkValidPath(folderPath.toString());
boolean hasRoleAdministrator = SecurityContextUtil.hasRole("ADMINISTRATOR");
if ("/".equals(folderPath.toString())) {
// Root folder
if (! hasRoleAdministrator) {
// Only administrator can create folders on root
throw new AccessDeniedException("No WRITE permission on /");
}
return null;
} else {
String folderNameToCheck = folderPath.getFileName().toString();
if (blacklistFolderNamePattern.matcher(folderNameToCheck).find()) {
throw new InvalidRepositoryPathException("Invalid folder name: " + folderNameToCheck);
}
PathValidator.checkValidFolderName(folderNameToCheck);
}
RepositoryFolder folder = folderRepository.findByPath(folderPath.toString());
if (folder == null) {
folder = new RepositoryFolder();
folder.setName(folderPath.getFileName().toString());
if (folderPath.getParent() != null) {
// This will assure permissions on existing parent folder
folder.setParent(ensureFolder(folderPath.getParent()));
}
folderRepository.save(folder);
} else {
// Assure permissions on folder if it already exists
if (! hasRoleAdministrator && ! SecurityContextUtil.hasPermission(folder, "WRITE")) {
throw new AccessDeniedException("No WRITE permission on " + folder.getPath());
}
}
return folder;
}
/*
* (non-Javadoc)
* @see
* org.genesys.filerepository.service.RepositoryService#deleteFolder(java.nio.
* file.Path)
*/
@Override
@Transactional(rollbackFor = Throwable.class)
public RepositoryFolder deleteFolder(Path path) throws FolderNotEmptyException, InvalidRepositoryPathException {
PathValidator.checkValidPath(path);
final Path cPath = path.normalize().toAbsolutePath();
RepositoryFolder folder = folderRepository.findByPath(cPath.toString());
if (folder != null) {
if (!folder.getChildren().isEmpty() || repositoryFilePersistence.countByFolder(folder) > 0) {
throw new FolderNotEmptyException("Folder " + path.toString() + " is not empty.");
}
folderRepository.delete(folder);
}
return folder;
}
/** {@inheritDoc} */
@Override
public byte[] getThumbnail(final Path path, String name, String extension, RepositoryFile repositoryFile) throws Exception {
assert (repositoryFile instanceof RepositoryImage);
String filename = name.concat(".").concat(extension);
try {
var data = bytesStorageService.get(path.resolve(filename));
if (data == null) {
var size = Integer.parseInt(name.split("x")[0]);
if (Arrays.stream(thumbnailSizes).noneMatch(availableSize -> availableSize == size)) {
throw new InvalidDataAccessApiUsageException("Size of thumbnail is invalid: ".concat(name));
}
// TODO Re-enable
// if (bytesStorageService.exists(errorFilename)) {
// throw new ThumbnailException(String.format("Thumbnail cannot be generated for image: %s. Filename: %s", repositoryFile.getUuid().toString(), filename));
// }
var imageBytes = getFileBytes(repositoryFile);
if (imageBytes == null) {
throw new NoSuchRepositoryFileException("No file for uuid: ".concat(repositoryFile.getUuid().toString()));
}
data = thumbnailGenerator.createThumbnail(size, size, extension, imageBytes);
try {
var errorFilename = RepositoryServiceImpl.getFullThumbnailsPath((RepositoryImage) repositoryFile).resolve(filename + "-err");
bytesStorageService.remove(errorFilename);
} catch (Throwable e) {
// Can't remove error file
}
bytesStorageService.upsert(RepositoryServiceImpl.getFullThumbnailsPath((RepositoryImage) repositoryFile).resolve(filename), data);
}
return data;
} catch (NoSuchRepositoryFileException e) {
log.warn(e.getMessage());
throw e;
} catch (InvalidDataAccessApiUsageException e) {
log.warn(e.getMessage());
throw new ThumbnailException(e.getMessage(), e);
} catch (Exception e) {
log.warn("Could not generate thumbnail: {}", e.getMessage());
// bytesStorageService.upsert(errorFilename, e.getMessage().getBytes());
throw e;
}
}
/**
* Sets the thumbnail sizes.
*
* @param thumbnailSizes the new thumbnail sizes
*/
public void setThumbnailSizes(int[] thumbnailSizes) {
Arrays.sort(thumbnailSizes);
this.thumbnailSizes = thumbnailSizes;
}
/**
* Helper to retrieve image bytes (and use loaded bytes when possible).
*/
public interface IImageBytesProvider {
/**
* Get image bytes.
* @throws IOException when things go wrong
* @return the bytes from storage
*/
byte[] getImageBytes() throws IOException;
}
/**
* Internal holder class for a Throwable in a callback transaction model.
*/
private static class ThrowableHolder<T extends Throwable> {
public T throwable;
}
private static void tryDelete(File target) {
log.debug("DELETING {}", target.getAbsolutePath());
try {
if (!target.delete()) {
log.debug("{} is not deleted", target.getAbsolutePath());
}
} catch (Exception e) {
log.debug("{} is not deleted", target.getAbsolutePath());
}
}
}