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.command;
020
021import com.google.inject.Inject;
022import com.plotsquared.core.backup.BackupManager;
023import com.plotsquared.core.backup.BackupProfile;
024import com.plotsquared.core.backup.NullBackupProfile;
025import com.plotsquared.core.backup.PlayerBackupProfile;
026import com.plotsquared.core.configuration.caption.TranslatableCaption;
027import com.plotsquared.core.exception.PlotSquaredException;
028import com.plotsquared.core.permissions.Permission;
029import com.plotsquared.core.player.PlotPlayer;
030import com.plotsquared.core.plot.Plot;
031import com.plotsquared.core.util.task.RunnableVal2;
032import com.plotsquared.core.util.task.RunnableVal3;
033import net.kyori.adventure.text.Component;
034import net.kyori.adventure.text.minimessage.tag.Tag;
035import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
036import org.apache.logging.log4j.LogManager;
037import org.apache.logging.log4j.Logger;
038import org.checkerframework.checker.nullness.qual.NonNull;
039
040import java.nio.file.Files;
041import java.time.Instant;
042import java.time.ZoneId;
043import java.time.ZonedDateTime;
044import java.time.format.DateTimeFormatter;
045import java.util.ArrayList;
046import java.util.Arrays;
047import java.util.Collection;
048import java.util.List;
049import java.util.Locale;
050import java.util.Objects;
051import java.util.concurrent.CompletableFuture;
052import java.util.stream.Collectors;
053import java.util.stream.IntStream;
054import java.util.stream.Stream;
055
056@CommandDeclaration(command = "backup",
057        usage = "/plot backup <save | list | load>",
058        category = CommandCategory.SETTINGS,
059        requiredType = RequiredType.PLAYER,
060        permission = "plots.backup")
061public final class Backup extends Command {
062
063    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + Backup.class.getSimpleName());
064
065    private final BackupManager backupManager;
066
067    @Inject
068    public Backup(final @NonNull BackupManager backupManager) {
069        super(MainCommand.getInstance(), true);
070        this.backupManager = backupManager;
071    }
072
073    private static boolean sendMessage(PlotPlayer<?> player) {
074        player.sendMessage(
075                TranslatableCaption.of("commandconfig.command_syntax"),
076                TagResolver.resolver("value", Tag.inserting(Component.text("/plot backup <save | list | load>")))
077        );
078        return true;
079    }
080
081    @Override
082    public CompletableFuture<Boolean> execute(
083            PlotPlayer<?> player, String[] args,
084            RunnableVal3<Command, Runnable, Runnable> confirm,
085            RunnableVal2<Command, CommandResult> whenDone
086    ) throws CommandException {
087        if (args.length == 0 || !Arrays.asList("save", "list", "load")
088                .contains(args[0].toLowerCase(Locale.ENGLISH))) {
089            return CompletableFuture.completedFuture(sendMessage(player));
090        }
091        return super.execute(player, args, confirm, whenDone);
092    }
093
094    @Override
095    public Collection<Command> tab(PlotPlayer<?> player, String[] args, boolean space) {
096        if (args.length == 1) {
097            return Stream.of("save", "list", "load")
098                    .filter(value -> value.startsWith(args[0].toLowerCase(Locale.ENGLISH)))
099                    .map(value -> new Command(null, false, value, "", RequiredType.NONE, null) {
100                    }).collect(Collectors.toList());
101        } else if (args[0].equalsIgnoreCase("load")) {
102
103            final Plot plot = player.getCurrentPlot();
104            if (plot != null) {
105                final BackupProfile backupProfile = Objects.requireNonNull(this.backupManager.getProfile(plot));
106                if (backupProfile instanceof PlayerBackupProfile) {
107                    final CompletableFuture<List<com.plotsquared.core.backup.Backup>> backupList =
108                            backupProfile.listBackups();
109                    if (backupList.isDone()) {
110                        final List<com.plotsquared.core.backup.Backup> backups =
111                                backupList.getNow(new ArrayList<>());
112                        if (backups.isEmpty()) {
113                            return new ArrayList<>();
114                        }
115                        return IntStream.range(1, 1 + backups.size()).mapToObj(
116                                i -> new Command(null, false, Integer.toString(i), "",
117                                        RequiredType.NONE, null
118                                ) {
119                                }).collect(Collectors.toList());
120
121                    }
122                }
123            }
124        }
125        return tabOf(player, args, space);
126    }
127
128    @CommandDeclaration(command = "save",
129            usage = "/plot backup save",
130            category = CommandCategory.SETTINGS,
131            requiredType = RequiredType.PLAYER,
132            permission = "plots.backup.save")
133    public void save(
134            final Command command, final PlotPlayer<?> player, final String[] args,
135            final RunnableVal3<Command, Runnable, Runnable> confirm,
136            final RunnableVal2<Command, CommandResult> whenDone
137    ) {
138        final Plot plot = player.getCurrentPlot();
139        if (plot == null) {
140            player.sendMessage(TranslatableCaption.of("errors.not_in_plot"));
141        } else if (!plot.hasOwner()) {
142            player.sendMessage(
143                    TranslatableCaption.of("backups.backup_impossible"),
144                    TagResolver.resolver("plot", Tag.inserting(
145                            TranslatableCaption.of("generic.generic_unowned").toComponent(player)
146                    ))
147            );
148        } else if (plot.getVolume() > Integer.MAX_VALUE) {
149            player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large"));
150        } else if (plot.isMerged()) {
151            player.sendMessage(
152                    TranslatableCaption.of("backups.backup_impossible"),
153                    TagResolver.resolver("plot", Tag.inserting(
154                            TranslatableCaption.of("generic.generic_merged").toComponent(player)
155                    ))
156            );
157        } else if (!plot.isOwner(player.getUUID()) && !player.hasPermission(Permission.PERMISSION_ADMIN_BACKUP_OTHER)) {
158            player.sendMessage(
159                    TranslatableCaption.of("permission.no_permission"),
160                    TagResolver.resolver(
161                            "node",
162                            Tag.inserting(Permission.PERMISSION_ADMIN_BACKUP_OTHER)
163                    )
164            );
165        } else {
166            final BackupProfile backupProfile = Objects.requireNonNull(this.backupManager.getProfile(plot));
167            if (backupProfile instanceof NullBackupProfile) {
168                player.sendMessage(
169                        TranslatableCaption.of("backups.backup_impossible"),
170                        TagResolver.resolver(
171                                "plot", Tag.inserting(TranslatableCaption
172                                        .of("generic.generic_other")
173                                        .toComponent(player))
174                        )
175                );
176            } else {
177                backupProfile.createBackup().whenComplete((backup, throwable) -> {
178                    if (throwable != null) {
179                        player.sendMessage(
180                                TranslatableCaption.of("backups.backup_save_failed"),
181                                TagResolver.resolver("reason", Tag.inserting(Component.text(throwable.getMessage())))
182                        );
183                        throwable.printStackTrace();
184                    } else {
185                        player.sendMessage(TranslatableCaption.of("backups.backup_save_success"));
186                    }
187                });
188            }
189        }
190    }
191
192    @CommandDeclaration(command = "list",
193            usage = "/plot backup list",
194            category = CommandCategory.SETTINGS,
195            requiredType = RequiredType.PLAYER,
196            permission = "plots.backup.list")
197    public void list(
198            final Command command, final PlotPlayer<?> player, final String[] args,
199            final RunnableVal3<Command, Runnable, Runnable> confirm,
200            final RunnableVal2<Command, CommandResult> whenDone
201    ) {
202        final Plot plot = player.getCurrentPlot();
203        if (plot == null) {
204            player.sendMessage(TranslatableCaption.of("errors.not_in_plot"));
205        } else if (!plot.hasOwner()) {
206            player.sendMessage(
207                    TranslatableCaption.of("backups.backup_impossible"),
208                    TagResolver.resolver("plot", Tag.inserting(
209                            TranslatableCaption.of("generic.generic_unowned").toComponent(player)
210                    ))
211            );
212        } else if (plot.isMerged()) {
213            player.sendMessage(
214                    TranslatableCaption.of("backups.backup_impossible"),
215                    TagResolver.resolver("plot", Tag.inserting(
216                            TranslatableCaption.of("generic.generic_merged").toComponent(player)
217                    ))
218            );
219        } else if (plot.getVolume() > Integer.MAX_VALUE) {
220            player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large"));
221        } else if (!plot.isOwner(player.getUUID()) && !player.hasPermission(Permission.PERMISSION_ADMIN_BACKUP_OTHER)) {
222            player.sendMessage(
223                    TranslatableCaption.of("permission.no_permission"),
224                    TagResolver.resolver(
225                            "node",
226                            Tag.inserting(Permission.PERMISSION_ADMIN_BACKUP_OTHER)
227                    )
228            );
229        } else {
230            final BackupProfile backupProfile = Objects.requireNonNull(this.backupManager.getProfile(plot));
231            if (backupProfile instanceof NullBackupProfile) {
232                player.sendMessage(
233                        TranslatableCaption.of("backups.backup_impossible"),
234                        TagResolver.resolver("plot", Tag.inserting(
235                                TranslatableCaption.of("generic.generic_other").toComponent(player)
236                        ))
237                );
238            } else {
239                backupProfile.listBackups().whenComplete((backups, throwable) -> {
240                    if (throwable != null) {
241                        player.sendMessage(
242                                TranslatableCaption.of("backups.backup_list_failed"),
243                                TagResolver.resolver("reason", Tag.inserting(Component.text(throwable.getMessage())))
244                        );
245                        throwable.printStackTrace();
246                    } else {
247                        player.sendMessage(
248                                TranslatableCaption.of("backups.backup_list_header"),
249                                TagResolver.resolver("plot", Tag.inserting(Component.text(plot.getId().toCommaSeparatedString())))
250                        );
251                        try {
252                            for (int i = 0; i < backups.size(); i++) {
253                                player.sendMessage(
254                                        TranslatableCaption.of("backups.backup_list_entry"),
255                                        TagResolver.builder()
256                                                .tag("number", Tag.inserting(Component.text(i + 1)))
257                                                .tag(
258                                                        "value",
259                                                        Tag.inserting(Component.text(DateTimeFormatter.RFC_1123_DATE_TIME.format(
260                                                                ZonedDateTime.ofInstant(
261                                                                        Instant.ofEpochMilli(backups.get(i).getCreationTime()),
262                                                                        ZoneId.systemDefault()
263                                                                ))))
264                                                )
265                                                .build()
266                                );
267                            }
268                        } catch (final Exception e) {
269                            e.printStackTrace();
270                        }
271                    }
272                });
273            }
274        }
275    }
276
277    @CommandDeclaration(command = "load",
278            usage = "/plot backup load <#>",
279            category = CommandCategory.SETTINGS,
280            requiredType = RequiredType.PLAYER,
281            permission = "plots.backup.load")
282    public void load(
283            final Command command, final PlotPlayer<?> player, final String[] args,
284            final RunnableVal3<Command, Runnable, Runnable> confirm,
285            final RunnableVal2<Command, CommandResult> whenDone
286    ) {
287        final Plot plot = player.getCurrentPlot();
288        if (plot == null) {
289            player.sendMessage(TranslatableCaption.of("errors.not_in_plot"));
290        } else if (!plot.hasOwner()) {
291            player.sendMessage(
292                    TranslatableCaption.of("backups.backup_impossible"),
293                    TagResolver.resolver("plot", Tag.inserting(
294                            TranslatableCaption.of("generic.generic_unowned").toComponent(player)
295                    ))
296            );
297        } else if (plot.isMerged()) {
298            player.sendMessage(
299                    TranslatableCaption.of("backups.backup_impossible"),
300                    TagResolver.resolver("plot", Tag.inserting(
301                            TranslatableCaption.of("generic.generic_merged").toComponent(player)
302                    ))
303            );
304        } else if (plot.getVolume() > Integer.MAX_VALUE) {
305            player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large"));
306        } else if (!plot.isOwner(player.getUUID()) && !player.hasPermission(Permission.PERMISSION_ADMIN_BACKUP_OTHER)) {
307            player.sendMessage(
308                    TranslatableCaption.of("permission.no_permission"),
309                    TagResolver.resolver(
310                            "node",
311                            Tag.inserting(Permission.PERMISSION_ADMIN_BACKUP_OTHER)
312                    )
313            );
314        } else if (args.length == 0) {
315            player.sendMessage(
316                    TranslatableCaption.of("commandconfig.command_syntax"),
317                    TagResolver.resolver("value", Tag.inserting(Component.text("Usage: /plot backup save/list/load")))
318            );
319        } else {
320            final int number;
321            try {
322                number = Integer.parseInt(args[0]);
323            } catch (final Exception e) {
324                player.sendMessage(
325                        TranslatableCaption.of("invalid.not_a_number"),
326                        TagResolver.resolver("value", Tag.inserting(Component.text(args[0])))
327                );
328                return;
329            }
330            final BackupProfile backupProfile = Objects.requireNonNull(this.backupManager.getProfile(plot));
331            if (backupProfile instanceof NullBackupProfile) {
332                player.sendMessage(
333                        TranslatableCaption.of("backups.backup_impossible"),
334                        TagResolver.resolver(
335                                "plot", Tag.inserting(
336                                        TranslatableCaption.of("generic.generic_other").toComponent(player)
337                                )
338                        )
339                );
340            } else {
341                backupProfile.listBackups().whenComplete((backups, throwable) -> {
342                    if (throwable != null) {
343                        Component reason;
344                        if (throwable instanceof PlotSquaredException pe) {
345                            reason = pe.getCaption().toComponent(player);
346                        } else {
347                            reason = Component.text(throwable.getMessage());
348                        }
349                        player.sendMessage(
350                                TranslatableCaption.of("backups.backup_load_failure"),
351                                TagResolver.resolver("reason", Tag.inserting(reason))
352                        );
353                        LOGGER.error("Error loading player ({}) backup", player.getName(), throwable);
354                        return;
355                    }
356                    if (number < 1 || number > backups.size()) {
357                        player.sendMessage(
358                                TranslatableCaption.of("backups.backup_impossible"),
359                                TagResolver.resolver(
360                                        "plot",
361                                        Tag.inserting(TranslatableCaption
362                                                .of("generic.generic_invalid_choice")
363                                                .toComponent(player))
364                                )
365                        );
366                    } else {
367                        final com.plotsquared.core.backup.Backup backup =
368                                backups.get(number - 1);
369                        if (backup == null || backup.getFile() == null || !Files
370                                .exists(backup.getFile())) {
371                            player.sendMessage(
372                                    TranslatableCaption.of("backups.backup_impossible"),
373                                    TagResolver.resolver(
374                                            "plot",
375                                            Tag.inserting(TranslatableCaption
376                                                    .of("generic.generic_invalid_choice")
377                                                    .toComponent(player))
378                                    )
379                            );
380                        } else {
381                            CmdConfirm.addPending(
382                                    player, "/plot backup load " + number,
383                                    () -> backupProfile.restoreBackup(backup, player)
384                                            .whenComplete((n, error) -> {
385                                                if (error != null) {
386                                                    player.sendMessage(
387                                                            TranslatableCaption.of("backups.backup_load_failure"),
388                                                            TagResolver.resolver(
389                                                                    "reason",
390                                                                    Tag.inserting(Component.text(error.getMessage()))
391                                                            )
392                                                    );
393                                                } else {
394                                                    player.sendMessage(TranslatableCaption.of("backups.backup_load_success"));
395                                                }
396                                            })
397                            );
398                        }
399                    }
400                });
401            }
402        }
403    }
404
405}