datasafe-storage/datasafe-storage-impl-fs/src/main/java/de/adorsys/datasafe/storage/impl/fs/FileSystemStorageService.java
package de.adorsys.datasafe.storage.impl.fs;
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
import de.adorsys.datasafe.storage.api.StorageService;
import de.adorsys.datasafe.types.api.callback.ResourceWriteCallback;
import de.adorsys.datasafe.types.api.resource.AbsoluteLocation;
import de.adorsys.datasafe.types.api.resource.AbsoluteLocationWithCapability;
import de.adorsys.datasafe.types.api.resource.BasePrivateResource;
import de.adorsys.datasafe.types.api.resource.BaseResolvedResource;
import de.adorsys.datasafe.types.api.resource.ResolvedResource;
import de.adorsys.datasafe.types.api.resource.StorageCapability;
import de.adorsys.datasafe.types.api.resource.Uri;
import de.adorsys.datasafe.types.api.resource.UriEncoderDecoder;
import de.adorsys.datasafe.types.api.resource.WithCallback;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.stream.Stream;
/**
* Filesystem ({@link java.nio.file}) compatible storage service default implementation.
* Note: Stores paths in URL-encoded form.
*/
@Slf4j
@RequiredArgsConstructor
public class FileSystemStorageService implements StorageService {
private final Uri dir;
public FileSystemStorageService(Path dir) {
this.dir = new Uri(dir.toUri());
}
/**
* Lists resources and returns their location without access credentials.
*/
@SneakyThrows
@Override
public Stream<AbsoluteLocation<ResolvedResource>> list(AbsoluteLocation path) {
log.debug("List file request: {}", path.location());
Path filePath = resolve(path.location().getRawPath(), false);
// FS should be compatible with s3 behavior:
if (!filePath.toFile().exists()) {
return Stream.empty();
}
boolean shouldReturnDir = shouldReturnDir(path);
return Files.walk(filePath)
// filter directories out based on setting
.filter(it -> (shouldReturnDir && allowableDir(it)) || !it.toFile().isDirectory())
.map(it -> new AbsoluteLocation<>(
new BaseResolvedResource(
// We store path in uri-encoded form, so toUri calls will fail
new BasePrivateResource(
new Uri(URI.create(UriEncoderDecoder.decodeAndDropAuthority(it.toUri())))
),
Instant.ofEpochMilli(it.toFile().lastModified()))
)
);
}
private boolean allowableDir(Path it) {
String name = it.getFileName().toString();
return !".".equals(name)
&& !"..".equals(name)
// prevents root folder to appear
&& !(it.toString() + "/").equals(dir.getRawPath());
}
private boolean shouldReturnDir(AbsoluteLocation path) {
return path instanceof AbsoluteLocationWithCapability
&& ((AbsoluteLocationWithCapability) path).getCapability().equals(StorageCapability.LIST_RETURNS_DIR);
}
@SneakyThrows
@Override
public InputStream read(AbsoluteLocation path) {
log.debug("Read file request: {}",path.location());
Path filePath = resolve(path.location().getRawPath(), false);
return MoreFiles.asByteSource(filePath, StandardOpenOption.READ).openStream();
}
@SneakyThrows
@Override
public OutputStream write(WithCallback<AbsoluteLocation, ? extends ResourceWriteCallback> locationWithCallback) {
log.debug("Write file request: {}", locationWithCallback.getWrapped().location());
Path filePath = resolve(locationWithCallback.getWrapped().location().getRawPath(), true);
log.debug("Write file: {}", locationWithCallback.getWrapped().location());
return MoreFiles
.asByteSink(filePath, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)
.openStream();
}
@SneakyThrows
@Override
public void remove(AbsoluteLocation location) {
if (!objectExists(location)) {
log.debug("nothing to delete {}", location.location());
return;
}
Path path = resolve(location.location().getRawPath(), false);
boolean isFile = !path.toFile().isDirectory();
MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE);
log.debug("deleted {} at: {}", isFile ? "file" : "directory", location.location());
}
@Override
public boolean objectExists(AbsoluteLocation location) {
boolean exists = Files.exists(resolve(location.location().getRawPath(), false));
log.debug("Exists {}: {}", location.location(), exists);
return exists;
}
protected Path resolve(String uriPath, boolean mkDirs) {
Path path = Paths.get(dir.resolve(uriPath).asURI());
if (!path.getParent().toFile().exists() && mkDirs) {
log.debug("Creating directories for: {}", uriPath);
path.getParent().toFile().mkdirs();
}
return Paths.get(dir.resolve(uriPath).asURI());
}
}