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.util;
020
021import com.google.inject.Inject;
022import com.intellectualsites.arkitektonika.Arkitektonika;
023import com.intellectualsites.arkitektonika.SchematicKeys;
024import com.plotsquared.core.PlotSquared;
025import com.plotsquared.core.configuration.Settings;
026import com.plotsquared.core.plot.Plot;
027import com.sk89q.jnbt.CompoundTag;
028import com.sk89q.jnbt.NBTOutputStream;
029import org.apache.logging.log4j.LogManager;
030import org.apache.logging.log4j.Logger;
031import org.checkerframework.checker.nullness.qual.NonNull;
032import org.checkerframework.checker.nullness.qual.Nullable;
033
034import java.io.IOException;
035import java.io.OutputStream;
036import java.nio.file.Files;
037import java.nio.file.Path;
038import java.nio.file.Paths;
039import java.util.concurrent.CompletableFuture;
040import java.util.concurrent.CompletionException;
041import java.util.zip.GZIPOutputStream;
042
043/**
044 * This class handles communication with the Arkitektonika REST service.
045 */
046public class PlotUploader {
047
048    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotUploader.class.getSimpleName());
049    private static final Path TEMP_DIR = Paths.get(PlotSquared.platform().getDirectory().getPath());
050    private final SchematicHandler schematicHandler;
051    private final Arkitektonika arkitektonika;
052
053    /**
054     * Create a new PlotUploader instance that uses the given schematic handler to create
055     * schematics of plots.
056     *
057     * @param schematicHandler the handler to create schematics of plots.
058     */
059    @Inject
060    public PlotUploader(final @NonNull SchematicHandler schematicHandler) {
061        this.schematicHandler = schematicHandler;
062        this.arkitektonika = Arkitektonika.builder().withUrl(Settings.Arkitektonika.BACKEND_URL).build();
063    }
064
065    /**
066     * Upload a plot and retrieve a result. The plot will be saved into a temporary
067     * schematic file and uploaded to the REST service
068     * specified by {@link Settings.Arkitektonika#BACKEND_URL}.
069     *
070     * @param plot The plot to upload
071     * @return a {@link CompletableFuture} that provides a {@link PlotUploadResult} if finished.
072     */
073    public CompletableFuture<PlotUploadResult> upload(final @NonNull Plot plot) {
074        return this.schematicHandler.getCompoundTag(plot)
075                .handle((tag, t) -> {
076                    plot.removeRunning();
077                    return tag;
078                })
079                .thenApply(this::writeToTempFile)
080                .thenApply(this::uploadAndDelete)
081                .thenApply(this::wrapIntoResult);
082    }
083
084    @NonNull
085    private PlotUploadResult wrapIntoResult(final @Nullable SchematicKeys schematicKeys) {
086        if (schematicKeys == null) {
087            return PlotUploadResult.failed();
088        }
089        String download = Settings.Arkitektonika.DOWNLOAD_URL.replace("{key}", schematicKeys.getAccessKey());
090        String delete = Settings.Arkitektonika.DELETE_URL.replace("{key}", schematicKeys.getDeletionKey());
091        return PlotUploadResult.success(download, delete);
092    }
093
094    @Nullable
095    private SchematicKeys uploadAndDelete(final @NonNull Path file) {
096        try {
097            final CompletableFuture<SchematicKeys> upload = this.arkitektonika.upload(file.toFile());
098            return upload.join();
099        } catch (CompletionException e) {
100            LOGGER.error("Failed to upload schematic", e);
101            return null;
102        } finally {
103            try {
104                Files.delete(file);
105            } catch (IOException e) {
106                LOGGER.error("Failed to delete temporary file {}", file, e);
107            }
108        }
109    }
110
111    @NonNull
112    private Path writeToTempFile(final @NonNull CompoundTag schematic) {
113        try {
114            final Path tempFile = Files.createTempFile(TEMP_DIR, null, null);
115            try (final OutputStream stream = Files.newOutputStream(tempFile)) {
116                writeSchematic(schematic, stream);
117            }
118            return tempFile;
119        } catch (IOException e) {
120            throw new RuntimeException(e);
121        }
122    }
123
124    /**
125     * Writes a schematic provided as CompoundTag to an OutputStream.
126     *
127     * @param schematic The schematic to write to the stream
128     * @param stream    The stream to write the schematic to
129     * @throws IOException if an I/O error occurred
130     */
131    private void writeSchematic(final @NonNull CompoundTag schematic, final @NonNull OutputStream stream)
132            throws IOException {
133        try (final NBTOutputStream nbtOutputStream = new NBTOutputStream(new GZIPOutputStream(stream))) {
134            nbtOutputStream.writeNamedTag("Schematic", schematic);
135        }
136    }
137
138    /**
139     * A result of a plot upload process.
140     */
141    public static class PlotUploadResult {
142
143        private final boolean success;
144        private final String downloadUrl;
145        private final String deletionUrl;
146
147        private PlotUploadResult(
148                boolean success, final @Nullable String downloadUrl,
149                final @Nullable String deletionUrl
150        ) {
151            this.success = success;
152            this.downloadUrl = downloadUrl;
153            this.deletionUrl = deletionUrl;
154        }
155
156        @NonNull
157        private static PlotUploadResult success(final @NonNull String downloadUrl, final @Nullable String deletionUrl) {
158            return new PlotUploadResult(true, downloadUrl, deletionUrl);
159        }
160
161        @NonNull
162        private static PlotUploadResult failed() {
163            return new PlotUploadResult(false, null, null);
164        }
165
166        /**
167         * Get whether this result is a success.
168         *
169         * @return {@code true} if this is a successful result, {@code false} otherwise.
170         */
171        public boolean isSuccess() {
172            return success;
173        }
174
175        /**
176         * Get the url that can be used to download the uploaded plot schematic.
177         *
178         * @return The url to download the schematic.
179         */
180        public String getDownloadUrl() {
181            return downloadUrl;
182        }
183
184        /**
185         * Get the url that can be used to delete the uploaded plot schematic.
186         *
187         * @return The url to delete the schematic.
188         */
189        public String getDeletionUrl() {
190            return deletionUrl;
191        }
192
193    }
194
195}