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}