All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.artipie.asto.fs.FileStorage Maven / Gradle / Ivy

/*
 * The MIT License (MIT) Copyright (c) 2020-2023 artipie.com
 * https://github.com/artipie/artipie/blob/master/LICENSE.txt
 */
package com.artipie.asto.fs;

import com.artipie.ArtipieException;
import com.artipie.asto.ArtipieIOException;
import com.artipie.asto.Content;
import com.artipie.asto.Key;
import com.artipie.asto.Meta;
import com.artipie.asto.OneTimePublisher;
import com.artipie.asto.Storage;
import com.artipie.asto.UnderLockOperation;
import com.artipie.asto.ValueNotFoundException;
import com.artipie.asto.ext.CompletableFutureSupport;
import com.artipie.asto.lock.storage.StorageLock;
import com.jcabi.log.Logger;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.cqfn.rio.file.File;

/**
 * Simple storage, in files.
 *
 * @since 0.1
 */
@SuppressWarnings("PMD.TooManyMethods")
public final class FileStorage implements Storage {

    /**
     * Where we keep the data.
     */
    private final Path dir;

    /**
     * Storage string identifier (name and path).
     */
    private final String id;

    /**
     * Ctor.
     * @param path The path to the dir
     * @param nothing Just for compatibility
     * @deprecated Use {@link FileStorage#FileStorage(Path)} ctor instead.
     */
    @Deprecated
    @SuppressWarnings("PMD.UnusedFormalParameter")
    public FileStorage(final Path path, final Object nothing) {
        this(path);
    }

    /**
     * Ctor.
     * @param path The path to the dir
     */
    public FileStorage(final Path path) {
        this.dir = path;
        this.id = String.format("FS: %s", this.dir.toString());
    }

    @Override
    public CompletableFuture exists(final Key key) {
        return this.keyPath(key).thenApplyAsync(
            path -> Files.exists(path) && !Files.isDirectory(path)
        );
    }

    @Override
    public CompletableFuture> list(final Key prefix) {
        return this.keyPath(prefix).thenApplyAsync(
            path -> {
                final Collection keys;
                if (Files.exists(path)) {
                    final int dirnamelen;
                    if (Key.ROOT.equals(prefix)) {
                        dirnamelen = path.toString().length() + 1;
                    } else {
                        dirnamelen = path.toString().length() - prefix.string().length();
                    }
                    try {
                        keys = Files.walk(path)
                            .filter(Files::isRegularFile)
                            .map(Path::toString)
                            .map(p -> p.substring(dirnamelen))
                            .map(
                                s -> s.split(
                                    FileSystems.getDefault().getSeparator().replace("\\", "\\\\")
                                )
                            )
                            .map(Key.From::new)
                            .sorted(Comparator.comparing(Key.From::string))
                            .collect(Collectors.toList());
                    } catch (final IOException iex) {
                        throw new ArtipieIOException(iex);
                    }
                } else {
                    keys = Collections.emptyList();
                }
                Logger.info(
                    this,
                    "Found %d objects by the prefix \"%s\" in %s by %s: %s",
                    keys.size(), prefix.string(), this.dir, path, keys
                );
                return keys;
            }
        );
    }

    @Override
    public CompletableFuture save(final Key key, final Content content) {
        return this.keyPath(key).thenApplyAsync(
            path ->  {
                final Path tmp = Paths.get(
                    this.dir.toString(),
                    String.format("%s.%s.tmp", key.string(), UUID.randomUUID())
                );
                tmp.getParent().toFile().mkdirs();
                return ImmutablePair.of(path, tmp);
            }
        ).thenCompose(
            pair -> {
                final Path path = pair.getKey();
                final Path tmp = pair.getValue();
                return new File(tmp).write(
                    new OneTimePublisher<>(content),
                    StandardOpenOption.WRITE,
                    StandardOpenOption.CREATE,
                    StandardOpenOption.TRUNCATE_EXISTING
                ).thenCompose(
                    nothing -> FileStorage.move(tmp, path)
                ).handleAsync(
                    (nothing, throwable) -> {
                        tmp.toFile().delete();
                        if (throwable == null) {
                            return null;
                        } else {
                            throw new ArtipieIOException(throwable);
                        }
                    }
                );
            }
        );
    }

    @Override
    public CompletableFuture move(final Key source, final Key destination) {
        return this.keyPath(source).thenCompose(
            src -> this.keyPath(destination).thenApply(dst -> ImmutablePair.of(src, dst))
        ).thenCompose(pair -> FileStorage.move(pair.getKey(), pair.getValue()));
    }

    @Override
    @SuppressWarnings("PMD.ExceptionAsFlowControl")
    public CompletableFuture delete(final Key key) {
        return this.keyPath(key).thenAcceptAsync(
            path -> {
                if (Files.exists(path) && !Files.isDirectory(path)) {
                    try {
                        Files.delete(path);
                        this.deleteEmptyParts(path.getParent());
                    } catch (final IOException iex) {
                        throw new ArtipieIOException(iex);
                    }
                } else {
                    throw new ValueNotFoundException(key);
                }
            }
        );
    }

    @Override
    public CompletableFuture metadata(final Key key) {
        return this.keyPath(key).thenApplyAsync(
            path -> {
                final BasicFileAttributes attrs;
                try {
                    attrs = Files.readAttributes(path, BasicFileAttributes.class);
                } catch (final NoSuchFileException fex) {
                    throw new ValueNotFoundException(key, fex);
                } catch (final IOException iox) {
                    throw new ArtipieIOException(iox);
                }
                return new FileMeta(attrs);
            }
        );
    }

    @Override
    public CompletableFuture value(final Key key) {
        final CompletableFuture res;
        if (Key.ROOT.string().equals(key.string())) {
            res = new CompletableFutureSupport.Failed(
                new ArtipieIOException("Unable to load from root")
            ).get();
        } else {
            res = this.metadata(key).thenApply(
                meta -> meta.read(Meta.OP_SIZE).orElseThrow(
                    () -> new ArtipieException(
                        String.format("Size is not available for '%s' key", key.string())
                    )
                )
            ).thenCompose(
                size -> this.keyPath(key).thenApply(path -> ImmutablePair.of(path, size))
            ).thenApply(
                pair -> new Content.OneTime(
                    new Content.From(pair.getValue(), new File(pair.getKey()).content())
                )
            );
        }
        return res;
    }

    @Override
    public  CompletionStage exclusively(
        final Key key,
        final Function> operation
    ) {
        return new UnderLockOperation<>(new StorageLock(this, key), operation).perform(this);
    }

    @Override
    public String identifier() {
        return this.id;
    }

    /**
     * Removes empty key parts (directories).
     * @param target Directory path
     */
    private void deleteEmptyParts(final Path target) {
        final Path dirabs = this.dir.normalize().toAbsolutePath();
        final Path path = target.normalize().toAbsolutePath();
        if (!path.toString().startsWith(dirabs.toString()) || dirabs.equals(path)) {
            return;
        }
        if (Files.isDirectory(path)) {
            boolean again = false;
            try {
                try (Stream files = Files.list(path)) {
                    if (!files.findFirst().isPresent()) {
                        Files.deleteIfExists(path);
                        again = true;
                    }
                }
                if (again) {
                    this.deleteEmptyParts(path.getParent());
                }
            } catch (final NoSuchFileException ex) {
                this.deleteEmptyParts(path.getParent());
            }
            catch (final IOException err) {
                throw new ArtipieIOException(err);
            }
        }
    }

    /**
     * Moves file from source path to destination.
     *
     * @param source Source path.
     * @param dest Destination path.
     * @return Completion of moving file.
     */
    private static CompletableFuture move(final Path source, final Path dest) {
        return CompletableFuture.supplyAsync(
            () -> {
                dest.getParent().toFile().mkdirs();
                return dest;
            }
        ).thenAcceptAsync(
            dst -> {
                try {
                    Files.move(source, dst, StandardCopyOption.REPLACE_EXISTING);
                } catch (final IOException iex) {
                    throw new ArtipieIOException(iex);
                }
            }
        );
    }

    /**
     * Converts key to path.
     * 

* Validates the path is in storage directory and converts it to path. * Fails with {@link ArtipieIOException} if key is out of storage location. *

* * @param key Key to validate. * @return Path future */ private CompletableFuture keyPath(final Key key) { final Path path = this.dir.resolve(key.string()); final CompletableFuture res = new CompletableFuture<>(); if (path.normalize().startsWith(path)) { res.complete(path); } else { res.completeExceptionally( new ArtipieIOException( String.format("Entry path is out of storage: %s", key) ) ); } return res; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy