Schem file limits (#773)

* added filesize/file count checks for /schematic save

* fixed /schematic save, added filesize component to /schematic list

* improved /schematics list total filesize output
need to add automatic conversion into Mb in the future

* temporary fix for page wrap from MessageBox

* reverted changed to MessageBox

* applied FAWE's checkstyle where applicable

* crude fix for worldedit MessageBox line wrapping happening sometimes

* small visual adjustments

* Update worldedit-core/src/main/java/com/boydti/fawe/config/Settings.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/boydti/fawe/config/Settings.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/boydti/fawe/config/Settings.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* Update worldedit-core/src/main/java/com/boydti/fawe/config/Settings.java

Co-authored-by: NotMyFault <mc.cache@web.de>

* fixed settings variable names

* Improved `/schematic save` success/error notifications
fixed local variable names

* removed unneeded comment

* fixed typo

* made schematics in subdirectories count towards file limit

Co-authored-by: NotMyFault <mc.cache@web.de>
This commit is contained in:
Ivan Volkov 2020-12-12 17:38:44 -05:00 committed by GitHub
parent 26e3e56988
commit bfc657d3f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 332 additions and 184 deletions

View File

@ -394,6 +394,18 @@ public class Settings extends Config {
" - Requires combine_stages = true" " - Requires combine_stages = true"
}) })
public boolean SEND_BEFORE_HISTORY = false; public boolean SEND_BEFORE_HISTORY = false;
@Comment({
"Sets a maximum limit (in kb) for the size of a player's schematics directory (per-player mode only)",
"Set to -1 to disable"
})
public int PER_PLAYER_FILE_SIZE_LIMIT = -1;
@Comment({
"Sets a maximum limit for the amount of schematics in a player's schematics directory (per-player mode only)",
"Set to -1 to disable"
})
public int PER_PLAYER_FILE_NUM_LIMIT = -1;
} }
public static class PLOTSQUARED_INTEGRATION { public static class PLOTSQUARED_INTEGRATION {

View File

@ -80,6 +80,7 @@ import java.net.URL;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -94,7 +95,6 @@ import static com.boydti.fawe.util.ReflectionUtils.as;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
//import com.boydti.fawe.object.schematic.visualizer.SchemVis;
/** /**
* Commands that work with schematic files. * Commands that work with schematic files.
@ -115,6 +115,31 @@ public class SchematicCommands {
this.worldEdit = worldEdit; this.worldEdit = worldEdit;
} }
//TODO filtering for directories, global, and private scheamtics needs to be reimplemented here
private static List<File> getFiles(File root, String filter, ClipboardFormat format) {
File[] files = root.listFiles();
if (files == null) {
return null;
}
//Only get the files that match the format parameter
if (format != null) {
files = Arrays.stream(files).filter(format::isFormat).toArray(File[]::new);
}
List<File> fileList = new ArrayList<>();
for (File f : files) {
if (f.isDirectory()) {
List<File> subFiles = getFiles(f, filter, format);
if (subFiles == null) {
continue; // empty subdir
}
fileList.addAll(subFiles);
} else {
fileList.add(f);
}
}
return fileList;
}
@Command( @Command(
name = "loadall", name = "loadall",
desc = "Load multiple clipboards (paste will randomly choose one)" desc = "Load multiple clipboards (paste will randomly choose one)"
@ -361,7 +386,7 @@ public class SchematicCommands {
ClipboardHolder holder = session.getClipboard(); ClipboardHolder holder = session.getClipboard();
SchematicSaveTask task = new SchematicSaveTask(actor, f, format, holder, overwrite); SchematicSaveTask task = new SchematicSaveTask(actor, f, dir, format, holder, overwrite);
AsyncCommandBuilder.wrap(task, actor) AsyncCommandBuilder.wrap(task, actor)
.registerWithSupervisor(worldEdit.getSupervisor(), "Saving schematic " + filename) .registerWithSupervisor(worldEdit.getSupervisor(), "Saving schematic " + filename)
.sendMessageAfterDelay(TranslatableComponent.of("worldedit.schematic.save.saving")) .sendMessageAfterDelay(TranslatableComponent.of("worldedit.schematic.save.saving"))
@ -559,173 +584,44 @@ public class SchematicCommands {
} }
msg.append(msgElem); msg.append(msgElem);
if (type == UtilityCommands.URIType.FILE) {
long filesize = 0;
try {
filesize = Files.size(Paths.get(dir.getAbsolutePath() + File.separator
+ (playerFolder ? (uuid.toString() + File.separator) : "") + path));
} catch (IOException e) {
e.printStackTrace();
}
TextComponent sizeElem = TextComponent.of(String.format(" (%.1f kb)", filesize / 1000.0), TextColor.GRAY);
msg.append(sizeElem);
}
return msg.create(); return msg.create();
}); });
PaginationBox paginationBox = PaginationBox.fromComponents("Available schematics", pageCommand, components);
long totalBytes = 0;
File parentDir = new File(dir.getAbsolutePath() + (playerFolder ? File.separator + uuid.toString() : ""));
try {
for (File schem : getFiles(parentDir, null, null)) {
if (schem.getName().endsWith(".schem") || schem.getName().endsWith(".schematic")) {
totalBytes += Files.size(Paths.get(schem.getAbsolutePath()));
}
}
} catch (IOException e) {
e.printStackTrace();
}
String headerBytesElem = String.format("%.1fkb", totalBytes / 1000.0);
if (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS && Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_SIZE_LIMIT > -1) {
headerBytesElem += String.format(" / %dkb",
Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_SIZE_LIMIT );
}
String fullHeader = "| Schematics: " + headerBytesElem + " |";
PaginationBox paginationBox = PaginationBox.fromComponents(fullHeader, pageCommand, components);
actor.print(paginationBox.create(page)); actor.print(paginationBox.create(page));
} }
private static class SchematicLoadTask implements Callable<ClipboardHolder> {
private final Actor actor;
private final File file;
private final ClipboardFormat format;
SchematicLoadTask(Actor actor, File file, ClipboardFormat format) {
this.actor = actor;
this.file = file;
this.format = format;
}
@Override
public ClipboardHolder call() throws Exception {
try (Closer closer = Closer.create()) {
FileInputStream fis = closer.register(new FileInputStream(file));
BufferedInputStream bis = closer.register(new BufferedInputStream(fis));
ClipboardReader reader = closer.register(format.getReader(bis));
Clipboard clipboard = reader.read();
log.info(actor.getName() + " loaded " + file.getCanonicalPath());
return new ClipboardHolder(clipboard);
}
}
}
private static class SchematicSaveTask implements Callable<Void> {
private final Actor actor;
private final File file;
private final ClipboardFormat format;
private final ClipboardHolder holder;
private final boolean overwrite;
SchematicSaveTask(Actor actor, File file, ClipboardFormat format, ClipboardHolder holder, boolean overwrite) {
this.actor = actor;
this.file = file;
this.format = format;
this.holder = holder;
this.overwrite = overwrite;
}
@Override
public Void call() throws Exception {
Clipboard clipboard = holder.getClipboard();
Transform transform = holder.getTransform();
Clipboard target;
// If we have a transform, bake it into the copy
if (transform.isIdentity()) {
target = clipboard;
} else {
FlattenedClipboardTransform result = FlattenedClipboardTransform.transform(clipboard, transform);
target = new BlockArrayClipboard(result.getTransformedRegion());
target.setOrigin(clipboard.getOrigin());
Operations.completeLegacy(result.copyTo(target));
}
try (Closer closer = Closer.create()) {
FileOutputStream fos = closer.register(new FileOutputStream(file));
BufferedOutputStream bos = closer.register(new BufferedOutputStream(fos));
ClipboardWriter writer = closer.register(format.getWriter(bos));
URI uri = null;
if (holder instanceof URIClipboardHolder) {
uri = ((URIClipboardHolder) holder).getURI(clipboard);
}
if (new ActorSaveClipboardEvent(actor, clipboard, uri, file.toURI()).call()) {
if (writer instanceof MinecraftStructure) {
((MinecraftStructure) writer).write(target, actor.getName());
} else {
writer.write(target);
}
log.info(actor.getName() + " saved " + file.getCanonicalPath());
actor.print(Caption.of("fawe.worldedit.schematic.schematic.saved", file.getName()));
} else {
actor.printError(TranslatableComponent.of("fawe.cancel.worldedit.cancel.reason.manual"));
}
}
return null;
}
}
private static class SchematicListTask implements Callable<Component> {
private final String prefix;
private final int sortType;
private final int page;
private final File rootDir;
private final String pageCommand;
private final String filter;
private String formatName;
SchematicListTask(String prefix, int sortType, int page, String pageCommand,
String filter, String formatName) {
this.prefix = prefix;
this.sortType = sortType;
this.page = page;
this.rootDir = WorldEdit.getInstance().getWorkingDirectoryFile(prefix);
this.pageCommand = pageCommand;
this.filter = filter;
this.formatName = formatName;
}
@Override
public Component call() throws Exception {
ClipboardFormat format = ClipboardFormats.findByAlias(formatName);
List<File> fileList = getFiles(rootDir,filter,format);
if (fileList == null || fileList.isEmpty()) {
return ErrorFormat.wrap("No schematics found.");
}
File[] files = new File[fileList.size()];
fileList.toArray(files);
// cleanup file list
Arrays.sort(files, (f1, f2) -> {
// http://stackoverflow.com/questions/203030/best-way-to-list-files-in-java-sorted-by-date-modified
int res;
if (sortType == 0) { // use name by default
int p = f1.getParent().compareTo(f2.getParent());
if (p == 0) { // same parent, compare names
res = f1.getName().compareTo(f2.getName());
} else { // different parent, sort by that
res = p;
}
} else {
res = Long.compare(f1.lastModified(), f2.lastModified()); // use date if there is a flag
if (sortType == 1) {
res = -res; // flip date for newest first instead of oldest first
}
}
return res;
});
PaginationBox paginationBox = new SchematicPaginationBox(prefix, files, pageCommand);
return paginationBox.create(page);
}
}
//TODO filtering for directories, global, and private scheamtics needs to be reimplemented here
private static List<File> getFiles(File root, String filter, ClipboardFormat format) {
File[] files = root.listFiles();
if (files == null) {
return null;
}
//Only get the files that match the format parameter
if (format != null) {
files = Arrays.stream(files).filter(format::isFormat).toArray(File[]::new);
}
List<File> fileList = new ArrayList<>();
for (File f : files) {
if (f.isDirectory()) {
List<File> subFiles = getFiles(f, filter, format);
if (subFiles == null) {
continue; // empty subdir
}
fileList.addAll(subFiles);
} else {
fileList.add(f);
}
}
return fileList;
}
@Command( @Command(
name = "delete", name = "delete",
aliases = {"d"}, aliases = {"d"},
@ -776,6 +672,246 @@ public class SchematicCommands {
return false; return false;
} }
private static class SchematicLoadTask implements Callable<ClipboardHolder> {
private final Actor actor;
private final ClipboardFormat format;
private final File file;
SchematicLoadTask(Actor actor, File file, ClipboardFormat format) {
this.actor = actor;
this.file = file;
this.format = format;
}
@Override
public ClipboardHolder call() throws Exception {
try (Closer closer = Closer.create()) {
FileInputStream fis = closer.register(new FileInputStream(file));
BufferedInputStream bis = closer.register(new BufferedInputStream(fis));
ClipboardReader reader = closer.register(format.getReader(bis));
Clipboard clipboard = reader.read();
log.info(actor.getName() + " loaded " + file.getCanonicalPath());
return new ClipboardHolder(clipboard);
}
}
}
private static class SchematicSaveTask implements Callable<Void> {
private final Actor actor;
private final ClipboardFormat format;
private final ClipboardHolder holder;
private final boolean overwrite;
private final File rootDir;
private File file;
SchematicSaveTask(Actor actor, File file, File rootDir, ClipboardFormat format, ClipboardHolder holder, boolean overwrite) {
this.actor = actor;
this.file = file;
this.rootDir = rootDir;
this.format = format;
this.holder = holder;
this.overwrite = overwrite;
}
@Override
public Void call() throws Exception {
Clipboard clipboard = holder.getClipboard();
Transform transform = holder.getTransform();
Clipboard target;
boolean checkFilesize = false;
if (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS
&& Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_SIZE_LIMIT > -1) {
checkFilesize = true;
}
double directorysizeKb = 0;
String curFilepath = file.getAbsolutePath();
final String SCHEMATIC_NAME = file.getName();
double oldKbOverwritten = 0;
String overwrittenPath = curFilepath;
int numFiles = -1;
if (checkFilesize) {
File parentDir = new File(file.getParent());
for (File child : getFiles(rootDir, null, null)) {
if (child.getName().endsWith(".schem") || child.getName().endsWith(".schematic")) {
directorysizeKb += Files.size(Paths.get(child.getAbsolutePath())) / 1000.0;
numFiles++;
}
}
if (overwrite) {
oldKbOverwritten = Files.size(Paths.get(file.getAbsolutePath())) / 1000.0;
int iter = 1;
while (new File(overwrittenPath + "." + iter + "." + format.getPrimaryFileExtension()).exists()) {
iter++;
}
file = new File(overwrittenPath + "." + iter + "." + format.getPrimaryFileExtension());
}
}
if (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS && Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_NUM_LIMIT > -1) {
if (numFiles == -1) {
numFiles = 0;
for (File child : getFiles(rootDir, null, null)) {
if (child.getName().endsWith(".schem") || child.getName().endsWith(".schematic")) {
numFiles++;
}
}
}
int limit = Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_NUM_LIMIT;
if (numFiles >= limit) {
TextComponent noSlotsErr = TextComponent.of( //TODO - to be moved into captions/translatablecomponents
String.format("You have " + numFiles + "/" + limit + " saved schematics. Delete some to save this one!",
TextColor.RED));
log.info(actor.getName() + " failed to save " + file.getCanonicalPath() + " - too many schematics!");
throw new WorldEditException(noSlotsErr) {
};
}
}
// If we have a transform, bake it into the copy
if (transform.isIdentity()) {
target = clipboard;
} else {
FlattenedClipboardTransform result = FlattenedClipboardTransform.transform(clipboard, transform);
target = new BlockArrayClipboard(result.getTransformedRegion());
target.setOrigin(clipboard.getOrigin());
Operations.completeLegacy(result.copyTo(target));
}
try (Closer closer = Closer.create()) {
FileOutputStream fos = closer.register(new FileOutputStream(file));
BufferedOutputStream bos = closer.register(new BufferedOutputStream(fos));
ClipboardWriter writer = closer.register(format.getWriter(bos));
URI uri = null;
if (holder instanceof URIClipboardHolder) {
uri = ((URIClipboardHolder) holder).getURI(clipboard);
}
if (new ActorSaveClipboardEvent(actor, clipboard, uri, file.toURI()).call()) {
if (writer instanceof MinecraftStructure) {
((MinecraftStructure) writer).write(target, actor.getName());
} else {
writer.write(target);
}
closer.close(); // release the new .schem file so that its size can be measured
double filesizeKb = Files.size(Paths.get(file.getAbsolutePath())) / 1000.0;
TextComponent filesizeNotif = TextComponent.of( //TODO - to be moved into captions/translatablecomponents
SCHEMATIC_NAME + " size: " + String.format("%.1f", filesizeKb) + "kb", TextColor.GRAY);
actor.print(filesizeNotif);
if (checkFilesize) {
double curKb = filesizeKb + directorysizeKb;
int allocatedKb = Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_SIZE_LIMIT;
if (overwrite) {
curKb -= oldKbOverwritten;
}
if ((curKb) > allocatedKb) {
file.delete();
TextComponent notEnoughKbErr = TextComponent.of( //TODO - to be moved into captions/translatablecomponents
"You're about to be at " + String.format("%.1f", curKb) + "kb of schematics. ("
+ String.format("%dkb", allocatedKb) + " available) Delete some first to save this one!",
TextColor.RED);
log.info(actor.getName() + " failed to save " + SCHEMATIC_NAME + " - not enough space!");
throw new WorldEditException(notEnoughKbErr) {
};
}
if (overwrite) {
new File(curFilepath).delete();
file.renameTo(new File(curFilepath));
} else {
numFiles++;
}
TextComponent kbRemainingNotif = TextComponent.of( //TODO - to be moved into captions/translatablecomponents
"You have " + String.format("%.1f", (allocatedKb - curKb)) + "kb left for schematics.", TextColor.GRAY);
actor.print(kbRemainingNotif);
}
if (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS && Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_NUM_LIMIT > -1) {
TextComponent slotsRemainingNotif = TextComponent.of( //TODO - to be moved into captions/translatablecomponents
"You have " + (Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_NUM_LIMIT - numFiles)
+ " schematic file slots left.", TextColor.GRAY);
actor.print(slotsRemainingNotif);
}
log.info(actor.getName() + " saved " + file.getCanonicalPath());
} else {
actor.printError(TranslatableComponent.of("fawe.cancel.worldedit.cancel.reason.manual"));
}
}
return null;
}
}
private static class SchematicListTask implements Callable<Component> {
private final String prefix;
private final int sortType;
private final int page;
private final File rootDir;
private final String pageCommand;
private final String filter;
private final String formatName;
SchematicListTask(String prefix, int sortType, int page, String pageCommand,
String filter, String formatName) {
this.prefix = prefix;
this.sortType = sortType;
this.page = page;
this.rootDir = WorldEdit.getInstance().getWorkingDirectoryFile(prefix);
this.pageCommand = pageCommand;
this.filter = filter;
this.formatName = formatName;
}
@Override
public Component call() throws Exception {
ClipboardFormat format = ClipboardFormats.findByAlias(formatName);
List<File> fileList = getFiles(rootDir, filter, format);
if (fileList == null || fileList.isEmpty()) {
return ErrorFormat.wrap("No schematics found.");
}
File[] files = new File[fileList.size()];
fileList.toArray(files);
// cleanup file list
Arrays.sort(files, (f1, f2) -> {
// http://stackoverflow.com/questions/203030/best-way-to-list-files-in-java-sorted-by-date-modified
int res;
if (sortType == 0) { // use name by default
int p = f1.getParent().compareTo(f2.getParent());
if (p == 0) { // same parent, compare names
res = f1.getName().compareTo(f2.getName());
} else { // different parent, sort by that
res = p;
}
} else {
res = Long.compare(f1.lastModified(), f2.lastModified()); // use date if there is a flag
if (sortType == 1) {
res = -res; // flip date for newest first instead of oldest first
}
}
return res;
});
PaginationBox paginationBox = new SchematicPaginationBox(prefix, files, pageCommand);
return paginationBox.create(page);
}
}
private static class SchematicPaginationBox extends PaginationBox { private static class SchematicPaginationBox extends PaginationBox {
private final String prefix; private final String prefix;
private final File[] files; private final File[] files;