software.coolstuff.springframework.owncloud.service.impl.local.OwncloudLocalResourceServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for software.coolstuff.springframework.owncloud.service.impl.local.OwncloudLocalResourceServiceImpl.java

Source

/*-
 * #%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
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #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<String, OwncloudLocalQuotaImpl> 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<Path> {
        private final Consumer<Long> 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<OwncloudResource> 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<OwncloudResource> 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<OwncloudResource> getDirectoryResources(Path location) {
        try {
            List<OwncloudResource> owncloudResources = new ArrayList<>();
            owncloudResources.add(getActualDirectoryOf(location));
            try (Stream<Path> 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<String> 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<OwncloudLocalResourceExtension> 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<OwncloudResource> 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<Path> {
        private final Consumer<Long> 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;
            });
        });
    }
}