001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.backup;
020
021import com.google.inject.Inject;
022import com.google.inject.assistedinject.Assisted;
023import com.plotsquared.core.configuration.caption.TranslatableCaption;
024import com.plotsquared.core.exception.PlotSquaredException;
025import com.plotsquared.core.player.PlotPlayer;
026import com.plotsquared.core.plot.Plot;
027import com.plotsquared.core.plot.schematic.Schematic;
028import com.plotsquared.core.util.SchematicHandler;
029import com.plotsquared.core.util.task.RunnableVal;
030import com.plotsquared.core.util.task.TaskManager;
031import org.apache.logging.log4j.LogManager;
032import org.apache.logging.log4j.Logger;
033import org.checkerframework.checker.nullness.qual.NonNull;
034import org.checkerframework.checker.nullness.qual.Nullable;
035
036import java.io.IOException;
037import java.nio.file.Files;
038import java.nio.file.Path;
039import java.nio.file.attribute.BasicFileAttributes;
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.Comparator;
043import java.util.List;
044import java.util.Objects;
045import java.util.UUID;
046import java.util.concurrent.CompletableFuture;
047
048/**
049 * A profile associated with a player (normally a plot owner) and a
050 * plot, which is used to store and retrieve plot backups
051 * {@inheritDoc}
052 */
053public class PlayerBackupProfile implements BackupProfile {
054
055    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlayerBackupProfile.class.getSimpleName());
056
057    private final UUID owner;
058    private final Plot plot;
059    private final BackupManager backupManager;
060    private final SchematicHandler schematicHandler;
061    private final Object backupLock = new Object();
062    private volatile List<Backup> backupCache;
063
064    @Inject
065    public PlayerBackupProfile(
066            @Assisted final @NonNull UUID owner, @Assisted final @NonNull Plot plot,
067            final @NonNull BackupManager backupManager, final @NonNull SchematicHandler schematicHandler
068    ) {
069        this.owner = owner;
070        this.plot = plot;
071        this.backupManager = backupManager;
072        this.schematicHandler = schematicHandler;
073    }
074
075    private static boolean isValidFile(final @NonNull Path path) {
076        final String name = path.getFileName().toString();
077        return name.endsWith(".schem") || name.endsWith(".schematic");
078    }
079
080    private static Path resolve(final @NonNull Path parent, final String child) {
081        Path path = parent;
082        try {
083            if (!Files.exists(parent)) {
084                Files.createDirectory(parent);
085            }
086            path = parent.resolve(child);
087            if (!Files.exists(path)) {
088                Files.createDirectory(path);
089            }
090        } catch (final Exception e) {
091            LOGGER.error("Error resolving {} from {}", child, parent, e);
092        }
093        return path;
094    }
095
096    @Override
097    public @NonNull CompletableFuture<List<Backup>> listBackups() {
098        synchronized (this.backupLock) {
099            if (this.backupCache != null) {
100                return CompletableFuture.completedFuture(backupCache);
101            }
102            return CompletableFuture.supplyAsync(() -> {
103                final Path path = this.getBackupDirectory();
104                if (!Files.exists(path)) {
105                    try {
106                        Files.createDirectories(path);
107                    } catch (IOException e) {
108                        LOGGER.error("Error creating directory {}", path, e);
109                        return Collections.emptyList();
110                    }
111                }
112                final List<Backup> backups = new ArrayList<>();
113                try {
114                    Files.walk(path).filter(PlayerBackupProfile::isValidFile).forEach(file -> {
115                        try {
116                            final BasicFileAttributes basicFileAttributes =
117                                    Files.readAttributes(file, BasicFileAttributes.class);
118                            backups.add(
119                                    new Backup(this, basicFileAttributes.creationTime().toMillis(), file));
120                        } catch (IOException e) {
121                            LOGGER.error("Error getting attributes for file {} to create backup", file, e);
122                        }
123                    });
124                } catch (IOException e) {
125                    LOGGER.error("Error walking files from {}", path, e);
126                }
127                backups.sort(Comparator.comparingLong(Backup::getCreationTime).reversed());
128                return (this.backupCache = backups);
129            });
130        }
131    }
132
133    @Override
134    public void destroy() {
135        this.listBackups().whenCompleteAsync((backups, error) -> {
136            if (error != null) {
137                LOGGER.error("Error while listing backups", error);
138            }
139            backups.forEach(Backup::delete);
140            this.backupCache = null;
141        });
142    }
143
144    public @NonNull Path getBackupDirectory() {
145        return resolve(
146                resolve(
147                        resolve(backupManager.getBackupPath(), Objects.requireNonNull(plot.getArea().toString(), "plot area id")),
148                        Objects.requireNonNull(plot.getId().toDashSeparatedString(), "plot id")
149                ), Objects.requireNonNull(owner.toString(), "owner")
150        );
151    }
152
153    @Override
154    public @NonNull CompletableFuture<Backup> createBackup() {
155        final CompletableFuture<Backup> future = new CompletableFuture<>();
156        this.listBackups().thenAcceptAsync(backups -> {
157            synchronized (this.backupLock) {
158                if (backups.size() == backupManager.getBackupLimit()) {
159                    backups.get(backups.size() - 1).delete();
160                }
161                final List<Plot> plots = Collections.singletonList(plot);
162                final boolean result = this.schematicHandler.exportAll(
163                        plots, getBackupDirectory().toFile(),
164                        "%world%-%id%-" + System.currentTimeMillis(), () ->
165                                future.complete(new Backup(this, System.currentTimeMillis(), null))
166                );
167                if (!result) {
168                    future.completeExceptionally(new RuntimeException("Failed to complete the backup"));
169                }
170                this.backupCache = null;
171            }
172        });
173        return future;
174    }
175
176    @Override
177    public @NonNull CompletableFuture<Void> restoreBackup(final @NonNull Backup backup, @Nullable PlotPlayer<?> player) {
178        final CompletableFuture<Void> future = new CompletableFuture<>();
179        if (backup.getFile() == null || !Files.exists(backup.getFile())) {
180            future.completeExceptionally(new IllegalArgumentException("The specific backup does not exist"));
181        } else {
182            TaskManager.runTaskAsync(() -> {
183                Schematic schematic = null;
184                try {
185                    schematic = this.schematicHandler.getSchematic(backup.getFile().toFile());
186                } catch (SchematicHandler.UnsupportedFormatException e) {
187                    LOGGER.error("Unsupported format for backup {}", backup.getFile(), e);
188                }
189                if (schematic == null) {
190                    future.completeExceptionally(new IllegalArgumentException(
191                            "The backup is non-existent or not in the correct format"));
192                } else {
193                    this.schematicHandler.paste(
194                            schematic,
195                            plot,
196                            0,
197                            plot.getArea().getMinBuildHeight(),
198                            0,
199                            false,
200                            player,
201                            new RunnableVal<>() {
202                                @Override
203                                public void run(Boolean value) {
204                                    if (value) {
205                                        future.complete(null);
206                                    } else {
207                                        future.completeExceptionally(new PlotSquaredException(
208                                                TranslatableCaption
209                                                        .of("schematics.schematic_paste_failed")));
210                                    }
211                                }
212                            }
213                    );
214                }
215            });
216        }
217        return future;
218    }
219
220}