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