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.player.ConsolePlayer;
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 net.kyori.adventure.text.minimessage.MiniMessage;
032import org.checkerframework.checker.nullness.qual.NonNull;
033import org.checkerframework.checker.nullness.qual.Nullable;
034
035import java.io.IOException;
036import java.nio.file.Files;
037import java.nio.file.Path;
038import java.nio.file.attribute.BasicFileAttributes;
039import java.util.ArrayList;
040import java.util.Collections;
041import java.util.Comparator;
042import java.util.List;
043import java.util.Objects;
044import java.util.UUID;
045import java.util.concurrent.CompletableFuture;
046
047/**
048 * A profile associated with a player (normally a plot owner) and a
049 * plot, which is used to store and retrieve plot backups
050 * {@inheritDoc}
051 */
052public class PlayerBackupProfile implements BackupProfile {
053
054    static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build();
055
056    private final UUID owner;
057    private final Plot plot;
058    private final BackupManager backupManager;
059    private final SchematicHandler schematicHandler;
060    private final Object backupLock = new Object();
061    private volatile List<Backup> backupCache;
062
063    @Inject
064    public PlayerBackupProfile(
065            @Assisted final @NonNull UUID owner, @Assisted final @NonNull Plot plot,
066            final @NonNull BackupManager backupManager, final @NonNull SchematicHandler schematicHandler
067    ) {
068        this.owner = owner;
069        this.plot = plot;
070        this.backupManager = backupManager;
071        this.schematicHandler = schematicHandler;
072    }
073
074    private static boolean isValidFile(final @NonNull Path path) {
075        final String name = path.getFileName().toString();
076        return name.endsWith(".schem") || name.endsWith(".schematic");
077    }
078
079    private static Path resolve(final @NonNull Path parent, final String child) {
080        Path path = parent;
081        try {
082            if (!Files.exists(parent)) {
083                Files.createDirectory(parent);
084            }
085            path = parent.resolve(child);
086            if (!Files.exists(path)) {
087                Files.createDirectory(path);
088            }
089        } catch (final Exception e) {
090            e.printStackTrace();
091        }
092        return path;
093    }
094
095    @Override
096    public @NonNull CompletableFuture<List<Backup>> listBackups() {
097        synchronized (this.backupLock) {
098            if (this.backupCache != null) {
099                return CompletableFuture.completedFuture(backupCache);
100            }
101            return CompletableFuture.supplyAsync(() -> {
102                final Path path = this.getBackupDirectory();
103                if (!Files.exists(path)) {
104                    try {
105                        Files.createDirectories(path);
106                    } catch (IOException e) {
107                        e.printStackTrace();
108                        return Collections.emptyList();
109                    }
110                }
111                final List<Backup> backups = new ArrayList<>();
112                try {
113                    Files.walk(path).filter(PlayerBackupProfile::isValidFile).forEach(file -> {
114                        try {
115                            final BasicFileAttributes basicFileAttributes =
116                                    Files.readAttributes(file, BasicFileAttributes.class);
117                            backups.add(
118                                    new Backup(this, basicFileAttributes.creationTime().toMillis(), file));
119                        } catch (IOException e) {
120                            e.printStackTrace();
121                        }
122                    });
123                } catch (IOException e) {
124                    e.printStackTrace();
125                }
126                backups.sort(Comparator.comparingLong(Backup::getCreationTime).reversed());
127                return (this.backupCache = backups);
128            });
129        }
130    }
131
132    @Override
133    public void destroy() {
134        this.listBackups().whenCompleteAsync((backups, error) -> {
135            if (error != null) {
136                error.printStackTrace();
137            }
138            backups.forEach(Backup::delete);
139            this.backupCache = null;
140        });
141    }
142
143    public @NonNull Path getBackupDirectory() {
144        return resolve(resolve(
145                resolve(backupManager.getBackupPath(), Objects.requireNonNull(plot.getArea().toString(), "plot area id")),
146                Objects.requireNonNull(plot.getId().toDashSeparatedString(), "plot id")
147        ), Objects.requireNonNull(owner.toString(), "owner"));
148    }
149
150    @Override
151    public @NonNull CompletableFuture<Backup> createBackup() {
152        final CompletableFuture<Backup> future = new CompletableFuture<>();
153        this.listBackups().thenAcceptAsync(backups -> {
154            synchronized (this.backupLock) {
155                if (backups.size() == backupManager.getBackupLimit()) {
156                    backups.get(backups.size() - 1).delete();
157                }
158                final List<Plot> plots = Collections.singletonList(plot);
159                final boolean result = this.schematicHandler.exportAll(plots, getBackupDirectory().toFile(),
160                        "%world%-%id%-" + System.currentTimeMillis(), () ->
161                                future.complete(new Backup(this, System.currentTimeMillis(), null))
162                );
163                if (!result) {
164                    future.completeExceptionally(new RuntimeException("Failed to complete the backup"));
165                }
166                this.backupCache = null;
167            }
168        });
169        return future;
170    }
171
172    @Override
173    public @NonNull CompletableFuture<Void> restoreBackup(final @NonNull Backup backup, @Nullable PlotPlayer<?> player) {
174        final CompletableFuture<Void> future = new CompletableFuture<>();
175        if (backup.getFile() == null || !Files.exists(backup.getFile())) {
176            future.completeExceptionally(new IllegalArgumentException("The specific backup does not exist"));
177        } else {
178            TaskManager.runTaskAsync(() -> {
179                Schematic schematic = null;
180                try {
181                    schematic = this.schematicHandler.getSchematic(backup.getFile().toFile());
182                } catch (SchematicHandler.UnsupportedFormatException e) {
183                    e.printStackTrace();
184                }
185                if (schematic == null) {
186                    future.completeExceptionally(new IllegalArgumentException(
187                            "The backup is non-existent or not in the correct format"));
188                } else {
189                    this.schematicHandler.paste(
190                            schematic,
191                            plot,
192                            0,
193                            plot.getArea().getMinBuildHeight(),
194                            0,
195                            false,
196                            player,
197                            new RunnableVal<>() {
198                                @Override
199                                public void run(Boolean value) {
200                                    if (value) {
201                                        future.complete(null);
202                                    } else {
203                                        future.completeExceptionally(new RuntimeException(MINI_MESSAGE.stripTokens(
204                                                TranslatableCaption
205                                                        .of("schematics.schematic_paste_failed")
206                                                        .getComponent(ConsolePlayer.getConsole()))));
207                                    }
208                                }
209                            }
210                    );
211                }
212            });
213        }
214        return future;
215    }
216
217}