software.coolstuff.springframework.owncloud.service.impl.local.OwncloudLocalResourceServiceImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of owncloud-spring-boot-starter Show documentation
Show all versions of owncloud-spring-boot-starter Show documentation
Spring Boot Owncloud Service and Authentication Provider
/*-
* #%L
* owncloud-spring-boot-starter
* %%
* Copyright (C) 2016 - 2017 by the original Authors
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* .
* #L%
*/
package software.coolstuff.springframework.owncloud.service.impl.local;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.util.UriComponentsBuilder;
import software.coolstuff.springframework.owncloud.exception.resource.OwncloudLocalResourceException;
import software.coolstuff.springframework.owncloud.exception.resource.OwncloudNoDirectoryResourceException;
import software.coolstuff.springframework.owncloud.exception.resource.OwncloudQuotaExceededException;
import software.coolstuff.springframework.owncloud.exception.resource.OwncloudResourceNotFoundException;
import software.coolstuff.springframework.owncloud.model.OwncloudFileResource;
import software.coolstuff.springframework.owncloud.model.OwncloudQuota;
import software.coolstuff.springframework.owncloud.model.OwncloudResource;
import software.coolstuff.springframework.owncloud.model.OwncloudUserDetails;
import software.coolstuff.springframework.owncloud.service.impl.OwncloudUtils;
import software.coolstuff.springframework.owncloud.service.impl.local.OwncloudLocalProperties.ResourceServiceProperties;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.FileNameMap;
import java.net.URI;
import java.net.URLConnection;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
class OwncloudLocalResourceServiceImpl implements OwncloudLocalResourceService {
@Autowired
private OwncloudLocalProperties properties;
@Autowired
private OwncloudLocalResourceChecksumService checksumService;
@Autowired
private OwncloudLocalUserDataService userDataService;
@Autowired
private OwncloudLocalUserServiceExtension userService;
private Map quotas = new HashMap<>();
@PostConstruct
protected void afterPropertiesSet() throws Exception {
OwncloudLocalProperties.ResourceServiceProperties resourceProperties = properties.getResourceService();
Validate.notNull(resourceProperties);
Validate.notNull(resourceProperties.getLocation());
Path baseLocation = resourceProperties.getLocation();
OwncloudLocalUtils.checkPrivilegesOnDirectory(baseLocation);
calculateQuotas(baseLocation);
log.debug("Register Usermodification Callbacks");
userService.registerSaveUserCallback(this::notifyUserModification);
userService.registerDeleteUserCallback(this::notifyRemovedUser);
}
private void calculateQuotas(Path baseLocation) throws IOException {
quotas.clear();
userDataService.getUsers()
.forEach(user -> {
String username = user.getUsername();
OwncloudLocalQuotaImpl quota = calculateUsedSpace(username, baseLocation);
quota.setTotal(user.getQuota());
quotas.put(username, quota);
});
}
private OwncloudLocalQuotaImpl calculateUsedSpace(String username, Path baseLocation) {
Path userBaseLocation = baseLocation.resolve(username);
if (Files.notExists(userBaseLocation)) {
return OwncloudLocalQuotaImpl.builder()
.username(username)
.location(baseLocation)
.build();
}
try {
OwncloudLocalQuotaImpl quota = OwncloudLocalQuotaImpl.builder()
.username(username)
.location(userBaseLocation)
.build();
log.debug("Calculate the Space used by User {}", username);
Files.walkFileTree(userBaseLocation, new UsedSpaceFileVisitor(quota::increaseUsed));
return quota;
} catch (IOException e) {
String logMessage = "IOException while calculating the used Space of Location " + userBaseLocation.toAbsolutePath().normalize().toString();
log.error(logMessage);
throw new OwncloudLocalResourceException(logMessage, e);
}
}
@RequiredArgsConstructor
private static class UsedSpaceFileVisitor extends SimpleFileVisitor {
private final Consumer usedSpaceIncreaseConsumer;
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
usedSpaceIncreaseConsumer.accept(Files.size(file));
return FileVisitResult.CONTINUE;
}
}
private void notifyUserModification(OwncloudUserDetails userDetails) {
log.debug("User {} has been changed or created -> change the Quota to {}", userDetails.getUsername(), userDetails.getQuota());
OwncloudLocalQuotaImpl quota = quotas.computeIfAbsent(userDetails.getUsername(), this::getOrCreateQuota);
quota.setTotal(userDetails.getQuota());
}
private OwncloudLocalQuotaImpl getOrCreateQuota(String username) {
ResourceServiceProperties resourceProperties = properties.getResourceService();
return calculateUsedSpace(username, resourceProperties.getLocation());
}
private void notifyRemovedUser(String username) {
log.debug("User {} has been removed -> remove the Quota Information", username);
quotas.remove(username);
}
@Override
public List list(URI relativeTo) {
Path location = resolveLocation(relativeTo);
return resourcesOf(location);
}
private Path resolveLocation(URI relativeTo) {
Path location = getRootLocationOfAuthenticatedUser();
if (relativeTo == null || StringUtils.isBlank(relativeTo.getPath())) {
return location;
}
createDirectoryIfNotExists(location);
String relativeToPath = relativeTo.getPath();
if (StringUtils.startsWith(relativeToPath, "/")) {
relativeToPath = relativeToPath.substring(1);
}
return location.resolve(relativeToPath);
}
private Path getRootLocationOfAuthenticatedUser() {
ResourceServiceProperties resourceProperties = properties.getResourceService();
Path location = resourceProperties.getLocation();
val username = getUsername();
location = location.resolve(username);
checkIfExistingDirectory(location);
log.debug("Resolved Base Location of User {}: {}", username, location);
return location;
}
private String getUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.getName();
}
private void checkIfExistingDirectory(Path location) {
if (isNotExistingDirectory(location)) {
val logMessage = String.format("Existing Path %s is not a Directory", location);
log.error(logMessage);
throw new OwncloudLocalResourceException(logMessage);
}
}
private boolean isNotExistingDirectory(Path location) {
return Files.exists(location) && !Files.isDirectory(location);
}
private void createDirectoryIfNotExists(Path location) {
if (Files.notExists(location)) {
try {
log.debug("Create Directory {}", location);
Files.createDirectories(location);
} catch (IOException e) {
val logMessage = String.format("Could not create Directory %s", location);
log.error(logMessage, e);
throw new OwncloudLocalResourceException(logMessage, e);
}
}
}
private List resourcesOf(Path location) {
if (Files.isDirectory(location)) {
return getDirectoryResources(location);
}
return Stream.of(createOwncloudResourceOf(location))
.peek(resource -> log.debug("Add Resource {} to the Result", resource.getHref()))
.collect(Collectors.toList());
}
private List getDirectoryResources(Path location) {
try {
List owncloudResources = new ArrayList<>();
owncloudResources.add(getActualDirectoryOf(location));
try (Stream stream = Files.list(location)) {
stream.map(this::createOwncloudResourceOf)
.peek(resource -> log.debug("Add Resource {} to the Result", resource.getHref()))
.forEach(owncloudResources::add);
}
getParentDirectoryOf(location)
.ifPresent(owncloudResources::add);
return owncloudResources;
} catch (IOException e) {
throw new OwncloudLocalResourceException(e);
}
}
private OwncloudLocalResourceExtension getActualDirectoryOf(Path location) {
OwncloudLocalResourceExtension actualDirectory = createOwncloudResourceOf(location);
log.debug("Add actual Directory {} to the Result", actualDirectory.getHref());
actualDirectory.setName(".");
return actualDirectory;
}
private OwncloudLocalResourceExtension createOwncloudResourceOf(Path path) {
Path rootPath = getRootLocationOfAuthenticatedUser();
Path relativePath = rootPath.toAbsolutePath().relativize(path.toAbsolutePath());
URI href = URI.create(
UriComponentsBuilder
.fromPath("/")
.path(relativePath.toString())
.toUriString());
String name = path.getFileName().toString();
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
if (Files.isDirectory(path)) {
href = URI.create(
UriComponentsBuilder.fromUri(href)
.path("/")
.toUriString());
mediaType = OwncloudUtils.getDirectoryMediaType();
} else {
FileNameMap fileNameMap = URLConnection.getFileNameMap();
String contentType = fileNameMap.getContentTypeFor(path.getFileName().toString());
if (StringUtils.isNotBlank(contentType)) {
mediaType = MediaType.valueOf(contentType);
}
}
try {
LocalDateTime lastModifiedAt = LocalDateTime.ofInstant(Files.getLastModifiedTime(path).toInstant(), ZoneId.systemDefault());
Optional checksum = checksumService.getChecksum(path);
if (Files.isSameFile(rootPath, path)) {
name = "/";
checksum = Optional.empty();
}
OwncloudLocalResourceExtension resource = OwncloudLocalResourceImpl.builder()
.href(href)
.name(name)
.eTag(checksum.orElse(null))
.mediaType(mediaType)
.lastModifiedAt(lastModifiedAt)
.build();
if (Files.isDirectory(path)) {
return resource;
}
return OwncloudLocalFileResourceImpl.fileBuilder()
.owncloudResource(resource)
.contentLength(Files.size(path))
.build();
} catch (NoSuchFileException e) {
throw new OwncloudResourceNotFoundException(href, getUsername());
} catch (IOException e) {
val logMessage = String.format("Cannot create OwncloudResource from Path %s", path);
log.error(logMessage, e);
throw new OwncloudLocalResourceException(logMessage, e);
}
}
private Optional getParentDirectoryOf(Path location) {
if (isParentDirectoryNotAppendable(location)) {
return Optional.empty();
}
OwncloudLocalResourceExtension superDirectory = createOwncloudResourceOf(location.resolve("..").normalize());
log.debug("Add parent Directory of Location {} ({}) to the Result", location, superDirectory.getHref());
superDirectory.setName("..");
return Optional.of(superDirectory);
}
private boolean isParentDirectoryNotAppendable(Path location) {
return !properties.getResourceService().isAddRelativeDownPath() || isRootDirectory(location);
}
private boolean isRootDirectory(Path location) {
if (!Files.isDirectory(location)) {
return false;
}
Path rootLocation = getRootLocationOfAuthenticatedUser();
try {
return Files.isSameFile(location, rootLocation);
} catch (IOException e) {
val logMessage = String.format("Cannot determine the equality of path %s to the base Location %s", location, rootLocation);
log.error(logMessage, e);
throw new OwncloudLocalResourceException(logMessage, e);
}
}
@Override
public Optional find(URI path) {
Path location = resolveLocation(path);
if (Files.notExists(location)) {
return Optional.empty();
}
log.debug("Get Information about Resource %s", path);
return Optional.ofNullable(createOwncloudResourceOf(location));
}
@Override
public OwncloudResource createDirectory(URI directory) {
Path location = resolveLocation(directory);
if (Files.exists(location)) {
if (Files.isDirectory(location)) {
return createOwncloudResourceOf(location);
}
throw new OwncloudNoDirectoryResourceException(directory);
}
try {
log.debug("Create Directory {}", location.toAbsolutePath().normalize());
Files.createDirectory(location);
checksumService.recalculateChecksum(location);
return createOwncloudResourceOf(location);
} catch (IOException e) {
val logMessage = String.format("Cannot create Directory %s", location.toAbsolutePath().normalize());
log.error(logMessage, e);
throw new OwncloudLocalResourceException(logMessage, e);
}
}
@Override
public void delete(OwncloudResource resource) {
Path path = resolveLocation(resource.getHref());
checkExistence(path, resource);
removeExistingPath(path);
}
private void checkExistence(Path path, OwncloudResource resource) {
if (Files.notExists(path)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
throw new OwncloudResourceNotFoundException(resource.getHref(), authentication.getName());
}
}
private void removeExistingPath(Path path) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OwncloudLocalQuotaImpl quota = quotas.get(authentication.getName());
removeExistingPathAndRecalculateSpaceAndChecksum(path, quota);
}
private void removeExistingPathAndRecalculateSpaceAndChecksum(Path path, OwncloudLocalQuotaImpl quota) {
try {
if (Files.isDirectory(path)) {
log.debug("Remove Directory {} with all its Content and reduce the used Space of User {}", path.toAbsolutePath().normalize(), quota.getUsername());
Files.walkFileTree(path, new DeleteFileVisitor(quota::reduceUsed));
} else {
log.debug("Remove File {} and reduce the used Space of User {}", path.toAbsolutePath().normalize(), quota.getUsername());
quota.reduceUsed(Files.size(path));
Files.delete(path);
}
} catch (IOException e) {
val logMessage = String.format("Cannot remove Path %s", path.toAbsolutePath().normalize());
log.error(logMessage, e);
throw new OwncloudLocalResourceException(logMessage, e);
} finally {
checksumService.recalculateChecksum(path);
}
}
@RequiredArgsConstructor
private static class DeleteFileVisitor extends SimpleFileVisitor {
private final Consumer usedSpaceReductionConsumer;
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
usedSpaceReductionConsumer.accept(Files.size(file));
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
}
@Override
public InputStream getInputStream(OwncloudFileResource resource) {
Path location = resolveLocation(resource.getHref());
try {
log.debug("Return InputStream of File {}", location.toAbsolutePath().normalize());
return Files.newInputStream(location);
} catch (NoSuchFileException e) {
log.warn("File {} not found", location.toAbsolutePath().normalize());
throw new OwncloudResourceNotFoundException(resource.getHref(), getUsername());
} catch (IOException e) {
val logMessage = String.format("Cannot get InputStream of File %s", location.toAbsolutePath().normalize());
log.error(logMessage, e);
throw new OwncloudLocalResourceException(logMessage, e);
}
}
@Override
public OutputStream getOutputStream(OwncloudFileResource resource) {
return getOutputStream(resource.getHref(), resource.getMediaType());
}
@Override
public OutputStream getOutputStream(URI path, MediaType mediaType) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.debug("Create a piped OutputStream to control the written Data (because of the Quota of User {}", authentication.getName());
PipedOutputStreamLocalSynchronizer pipedStreamSynchronizer = PipedOutputStreamLocalSynchronizer.builder()
.authentication(authentication)
.afterCopyCallback(this::afterCopy)
.owncloudLocalProperties(properties)
.uri(path)
.uriResolver(this::resolveLocation)
.build();
return pipedStreamSynchronizer.getOutputStream();
}
private void afterCopy(PipedOutputStreamAfterCopyEnvironment environment) {
Optional
.ofNullable(quotas.get(environment.getUsername()))
.ifPresent(quota -> {
checkSpace(quota, environment);
quota.increaseUsed(environment.getContentLength());
});
if (Files.exists(environment.getPath())) {
checksumService.recalculateChecksum(environment.getPath());
}
}
private void checkSpace(OwncloudQuota quota, PipedOutputStreamAfterCopyEnvironment environment) {
if (isNoMoreSpaceLeft(quota, environment)) {
log.error("User {} exceeded its Quota of {} Bytes", quota.getUsername(), quota.getTotal());
removeFile(environment);
throw new OwncloudQuotaExceededException(environment.getUri(), environment.getUsername());
}
}
private boolean isNoMoreSpaceLeft(OwncloudQuota quota, PipedOutputStreamAfterCopyEnvironment environment) {
return quota.getFree() < environment.getContentLength();
}
private void removeFile(PipedOutputStreamAfterCopyEnvironment environment) {
try {
log.debug("Remove File {}", environment.getPath().toAbsolutePath().normalize());
Files.delete(environment.getPath());
} catch (IOException e) {
final String logMessage = String.format("Error while removing File %s", environment.getPath().toAbsolutePath().normalize());
log.error(logMessage, e);
throw new OwncloudLocalResourceException(logMessage, e);
}
}
@Override
public OwncloudQuota getQuota() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.debug("Return the actual Quota of User {}", authentication.getName());
return quotas.get(authentication.getName());
}
@Override
public void resetAllUsedSpace() {
quotas.forEach(this::resetUsedSpace);
}
private void resetUsedSpace(String username, OwncloudLocalQuotaImpl quota) {
log.debug("Reset the used Space of User {}", username);
quota.setUsed(0);
}
@Override
public void recalculateAllUsedSpace() {
ResourceServiceProperties resourceProperties = properties.getResourceService();
Path baseLocation = resourceProperties.getLocation();
quotas.forEach((username, unusedQuota) -> {
quotas.computeIfPresent(username, (unusedUsername, existingQuota) -> {
OwncloudLocalQuotaImpl quota = calculateUsedSpace(username, baseLocation);
quota.setTotal(existingQuota.getTotal());
return quota;
});
});
}
}