From 527573e71bfc397d31374e60dda95c5597a602c3 Mon Sep 17 00:00:00 2001 From: sk89q Date: Wed, 20 Oct 2010 16:15:20 -0700 Subject: [PATCH] Added support for restoring from backups (snapshots). Added /listsnapshots, //use, and //restore. --- src/EditSession.java | 59 ------ src/SMWorldEdit.java | 6 + src/SnapshotRestore.java | 191 ++++++++++++++++++ src/WorldEdit.java | 127 ++++++++++++ src/WorldEditSession.java | 16 ++ .../snapshots/InvalidSnapshotException.java | 28 +++ .../sk89q/worldedit/snapshots/Snapshot.java | 73 +++++++ .../snapshots/SnapshotRepository.java | 144 +++++++++++++ 8 files changed, 585 insertions(+), 59 deletions(-) create mode 100644 src/SnapshotRestore.java create mode 100644 src/com/sk89q/worldedit/snapshots/InvalidSnapshotException.java create mode 100644 src/com/sk89q/worldedit/snapshots/Snapshot.java create mode 100644 src/com/sk89q/worldedit/snapshots/SnapshotRepository.java diff --git a/src/EditSession.java b/src/EditSession.java index 001fa2e2f..eba1f3242 100644 --- a/src/EditSession.java +++ b/src/EditSession.java @@ -1279,63 +1279,4 @@ public class EditSession { return affected; } - - /** - * Restores a region from a backup. - * - * @param region - * @param chunkStore - */ - public void restoreBackup(Region region, ChunkStore chunkStore) - throws MaxChangedBlocksException { - // TODO: Make this support non-cuboid regions - - Vector min = region.getMinimumPoint(); - Vector max = region.getMaximumPoint(); - - Map> neededChunks = - new LinkedHashMap>(); - - // First, we need to group points by chunk so that we only need - // to keep one chunk in memory at any given moment - for (int x = min.getBlockX(); x <= max.getBlockX(); x++) { - for (int y = min.getBlockY(); y <= max.getBlockY(); y++) { - for (int z = min.getBlockZ(); z <= max.getBlockZ(); z++) { - Vector pos = new Vector(x, y, z); - BlockVector2D chunkPos = ChunkStore.toChunk(pos); - - // Unidentified chunk - if (!neededChunks.containsKey(chunkPos)) { - neededChunks.put(chunkPos, new ArrayList()); - } - - neededChunks.get(chunkPos).add(pos); - } - } - } - - // Now let's start restoring! - for (Map.Entry> entry : - neededChunks.entrySet()) { - BlockVector2D chunkPos = entry.getKey(); - Chunk chunk; - - try { - chunk = chunkStore.getChunk(chunkPos); - // Good, the chunk could be at least loaded - - // Now just copy blocks! - for (Vector pos : entry.getValue()) { - BaseBlock block = chunk.getBlock(pos); - setBlock(pos, block); - } - } catch (DataException de) { - // TODO: Error handling - de.printStackTrace(); - } catch (IOException ioe) { - // TODO: Error handling - ioe.printStackTrace(); - } - } - } } diff --git a/src/SMWorldEdit.java b/src/SMWorldEdit.java index 7f70adcf4..9f1459d62 100644 --- a/src/SMWorldEdit.java +++ b/src/SMWorldEdit.java @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import com.sk89q.worldedit.snapshots.SnapshotRepository; import java.util.Map; import java.util.HashSet; import com.sk89q.worldedit.ServerInterface; @@ -86,6 +87,11 @@ public class SMWorldEdit extends Plugin { worldEdit.setDefaultChangeLimit( Math.max(-1, properties.getInt("max-blocks-changed", -1))); + String snapshotsDir = properties.getString("snapshots-dir", ""); + if (!snapshotsDir.trim().equals("")) { + worldEdit.setSnapshotRepository(new SnapshotRepository(snapshotsDir)); + } + for (Map.Entry entry : worldEdit.getCommands().entrySet()) { etc.getInstance().addCommand(entry.getKey(), entry.getValue()); } diff --git a/src/SnapshotRestore.java b/src/SnapshotRestore.java new file mode 100644 index 000000000..6f896c3af --- /dev/null +++ b/src/SnapshotRestore.java @@ -0,0 +1,191 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import com.sk89q.worldedit.*; +import com.sk89q.worldedit.regions.*; +import com.sk89q.worldedit.blocks.*; +import com.sk89q.worldedit.data.*; +import java.io.IOException; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ArrayList; + +/** + * + * @author sk89q + */ +public class SnapshotRestore { + /** + * Store a list of chunks that are needed and the points in them. + */ + private Map> neededChunks = + new LinkedHashMap>(); + /** + * Chunk store. + */ + private ChunkStore chunkStore; + /** + * Count of the number of missing chunks. + */ + private ArrayList missingChunks; + /** + * Count of the number of chunks that could be loaded for other reasons. + */ + private ArrayList errorChunks; + + /** + * Construct the snapshot restore operation. + * + * @param region + */ + public SnapshotRestore(ChunkStore chunkStore, Region region) { + this.chunkStore = chunkStore; + + if (region instanceof CuboidRegion) { + findNeededCuboidChunks(region); + } else { + findNeededChunks(region); + } + } + + /** + * Find needed chunks in the cuboid of the region. + * + * @param region + */ + private void findNeededCuboidChunks(Region region) { + Vector min = region.getMinimumPoint(); + Vector max = region.getMaximumPoint(); + + // First, we need to group points by chunk so that we only need + // to keep one chunk in memory at any given moment + for (int x = min.getBlockX(); x <= max.getBlockX(); x++) { + for (int y = min.getBlockY(); y <= max.getBlockY(); y++) { + for (int z = min.getBlockZ(); z <= max.getBlockZ(); z++) { + Vector pos = new Vector(x, y, z); + BlockVector2D chunkPos = ChunkStore.toChunk(pos); + + // Unidentified chunk + if (!neededChunks.containsKey(chunkPos)) { + neededChunks.put(chunkPos, new ArrayList()); + } + + neededChunks.get(chunkPos).add(pos); + } + } + } + } + + /** + * Find needed chunks in the region. + * + * @param region + */ + private void findNeededChunks(Region region) { + // First, we need to group points by chunk so that we only need + // to keep one chunk in memory at any given moment + for (Vector pos : region) { + BlockVector2D chunkPos = ChunkStore.toChunk(pos); + + // Unidentified chunk + if (!neededChunks.containsKey(chunkPos)) { + neededChunks.put(chunkPos, new ArrayList()); + } + + neededChunks.get(chunkPos).add(pos); + } + } + + /** + * Get the number of chunks that are needed. + * + * @return + */ + public int getChunksAffected() { + return neededChunks.size(); + } + + /** + * Restores to world. + * + * @param editSession + * @param region + */ + public void restore(EditSession editSession) + throws MaxChangedBlocksException { + + missingChunks = new ArrayList(); + errorChunks = new ArrayList(); + + // Now let's start restoring! + for (Map.Entry> entry : + neededChunks.entrySet()) { + BlockVector2D chunkPos = entry.getKey(); + Chunk chunk; + + try { + chunk = chunkStore.getChunk(chunkPos); + // Good, the chunk could be at least loaded + + // Now just copy blocks! + for (Vector pos : entry.getValue()) { + BaseBlock block = chunk.getBlock(pos); + editSession.setBlock(pos, block); + } + } catch (MissingChunkException me) { + missingChunks.add(chunkPos); + } catch (DataException de) { + errorChunks.add(chunkPos); + } catch (IOException ioe) { + errorChunks.add(chunkPos); + } + } + } + + /** + * Get a list of the missing chunks. restore() must have been called + * already. + * + * @return + */ + public List getMissingChunks() { + return missingChunks; + } + + /** + * Get a list of the chunks that could not have been loaded for other + * reasons. restore() must have been called already. + * + * @return + */ + public List getErrorChunks() { + return errorChunks; + } + + /** + * Checks to see where the backup succeeded in any capacity. False will + * be returned if no chunk could be successfully loaded. + * + * @return + */ + public boolean hadTotalFailure() { + return missingChunks.size() + errorChunks.size() == getChunksAffected(); + } +} diff --git a/src/WorldEdit.java b/src/WorldEdit.java index b2cc01f07..2623d5542 100644 --- a/src/WorldEdit.java +++ b/src/WorldEdit.java @@ -17,10 +17,13 @@ * along with this program. If not, see . */ +import com.sk89q.worldedit.snapshots.SnapshotRepository; +import com.sk89q.worldedit.snapshots.Snapshot; import com.sk89q.worldedit.*; import com.sk89q.worldedit.blocks.*; import com.sk89q.worldedit.data.*; import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.snapshots.InvalidSnapshotException; import java.util.HashMap; import java.util.HashSet; import java.util.Arrays; @@ -74,6 +77,10 @@ public class WorldEdit { * Default block change limit. -1 for no limit. */ private int defaultChangeLimit = -1; + /** + * Stores the snapshot repository. May be null; + */ + private SnapshotRepository snapshotRepo; /** * Set up an instance. @@ -162,6 +169,9 @@ public class WorldEdit { commands.put("/thru", "Go through the wall that you are looking at"); commands.put("/ceil", " - Get to the ceiling"); commands.put("/chunkinfo", "Get the filename of the chunk that you are in"); + commands.put("/listsnapshots", "List the 5 newest snapshots"); + commands.put("//use", "[SnapshotID] - Use a particular snapshot"); + commands.put("//restore", "Restore a particular snapshot"); } /** @@ -876,6 +886,109 @@ public class WorldEdit { player.print("Chunk: " + chunkX + ", " + chunkZ); player.print(folder1 + "/" + folder2 + "/" + filename); + return true; + + // List snapshots + } else if (split[0].equalsIgnoreCase("/listsnapshots")) { + checkArgs(split, 0, 0, split[0]); + + if (snapshotRepo != null) { + Snapshot[] snapshots = snapshotRepo.getSnapshots(); + + if (snapshots.length > 0) { + for (byte i = 0; i < Math.min(5, snapshots.length); i++) { + player.print((i + 1) + ". " + snapshots[i].getName()); + } + + player.print("Use //use [snapshot] or //use latest to set the snapshot."); + } else { + player.printError("No snapshots are available."); + } + } else { + player.printError("Snapshot/backup restore is not configured."); + } + + return true; + + // Use a certain snapshot + } else if (split[0].equalsIgnoreCase("//use")) { + checkArgs(split, 1, 1, split[0]); + + if (snapshotRepo == null) { + player.printError("Snapshot/backup restore is not configured."); + return true; + } + + String name = split[1]; + + // Want the latest snapshot? + if (name.equalsIgnoreCase("latest")) { + Snapshot snapshot = snapshotRepo.getDefaultSnapshot(); + + if (snapshot != null) { + session.setSnapshot(null); + player.print("Now using newest snapshot."); + } else { + player.printError("No snapshots were found."); + } + } else { + try { + session.setSnapshot(snapshotRepo.getSnapshot(name)); + player.print("Snapshot set to: " + name); + } catch (InvalidSnapshotException e) { + player.printError("That snapshot does not exist or is not available."); + } + } + + return true; + + // Restore + } else if (split[0].equalsIgnoreCase("//restore")) { + checkArgs(split, 0, 0, split[0]); + + if (snapshotRepo == null) { + player.printError("Snapshot/backup restore is not configured."); + return true; + } + + Region region = session.getRegion(); + Snapshot snapshot = session.getSnapshot(); + ChunkStore chunkStore; + + // No snapshot set? + if (snapshot == null) { + snapshot = snapshotRepo.getDefaultSnapshot(); + + if (snapshot == null) { + player.printError("No snapshots were found."); + return true; + } + } + + // Load chunk store + try { + chunkStore = snapshot.getChunkStore(); + player.print("Snapshot '" + snapshot.getName() + "' loaded; now restoring..."); + } catch (IOException e) { + player.printError("Failed to load snapshot: " + e.getMessage()); + return true; + } + + // Restore snapshot + SnapshotRestore restore = new SnapshotRestore(chunkStore, region); + //player.print(restore.getChunksAffected() + " chunk(s) will be loaded."); + + restore.restore(editSession); + + if (restore.hadTotalFailure()) { + player.printError("No blocks could be restored. (Bad backup?)"); + } else { + player.print(String.format("Restored; %d " + + "missing chunks and %d other errors.", + restore.getMissingChunks().size(), + restore.getErrorChunks().size())); + } + return true; } @@ -994,4 +1107,18 @@ public class WorldEdit { public void setDefaultChangeLimit(int defaultChangeLimit) { this.defaultChangeLimit = defaultChangeLimit; } + + /** + * @return the snapshotRepo + */ + public SnapshotRepository getSnapshotRepo() { + return snapshotRepo; + } + + /** + * @param snapshotRepo the snapshotRepo to set + */ + public void setSnapshotRepository(SnapshotRepository snapshotRepo) { + this.snapshotRepo = snapshotRepo; + } } diff --git a/src/WorldEditSession.java b/src/WorldEditSession.java index b6389eba4..dd1f1880c 100644 --- a/src/WorldEditSession.java +++ b/src/WorldEditSession.java @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import com.sk89q.worldedit.snapshots.Snapshot; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.*; @@ -37,6 +38,7 @@ public class WorldEditSession { private boolean toolControl = true; private boolean superPickAxe = false; private int maxBlocksChanged = -1; + private Snapshot snapshot; /** * Clear history. @@ -305,4 +307,18 @@ public class WorldEditSession { placeAtPos1 = !placeAtPos1; return placeAtPos1; } + + /** + * @return the snapshotName + */ + public Snapshot getSnapshot() { + return snapshot; + } + + /** + * @param snapshot + */ + public void setSnapshot(Snapshot snapshot) { + this.snapshot = snapshot; + } } diff --git a/src/com/sk89q/worldedit/snapshots/InvalidSnapshotException.java b/src/com/sk89q/worldedit/snapshots/InvalidSnapshotException.java new file mode 100644 index 000000000..172b1892e --- /dev/null +++ b/src/com/sk89q/worldedit/snapshots/InvalidSnapshotException.java @@ -0,0 +1,28 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.snapshots; + +/** + * + * @author sk89q + */ +public class InvalidSnapshotException extends Exception { + +} diff --git a/src/com/sk89q/worldedit/snapshots/Snapshot.java b/src/com/sk89q/worldedit/snapshots/Snapshot.java new file mode 100644 index 000000000..795f73966 --- /dev/null +++ b/src/com/sk89q/worldedit/snapshots/Snapshot.java @@ -0,0 +1,73 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.snapshots; + +import com.sk89q.worldedit.data.*; +import java.io.*; + +/** + * + * @author sk89q + */ +public class Snapshot { + /** + * Stores snapshot file. + */ + private File file; + /** + * Name of the snapshot; + */ + private String name; + + /** + * Construct a snapshot restoration operation. + * + * @param editSession + * @param dir + * @param snapshot + */ + public Snapshot(SnapshotRepository repo, String snapshot) { + file = new File(repo.getDirectory(), snapshot); + name = snapshot; + } + + /** + * Get a chunk store. + * + * @return + * @throws IOException + */ + public ChunkStore getChunkStore() throws IOException { + if (file.getName().toLowerCase().endsWith(".zip")) { + return new ZippedAlphaChunkStore(file); + } else { + return new AlphaChunkStore(file); + } + } + + /** + * Get the snapshot's name. + * + * @return + */ + public String getName() { + return name; + } +} diff --git a/src/com/sk89q/worldedit/snapshots/SnapshotRepository.java b/src/com/sk89q/worldedit/snapshots/SnapshotRepository.java new file mode 100644 index 000000000..89f7af559 --- /dev/null +++ b/src/com/sk89q/worldedit/snapshots/SnapshotRepository.java @@ -0,0 +1,144 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.snapshots; + +import java.io.*; +import java.util.Arrays; +import java.util.Collections; + +/** + * + * @author sk89q + */ +public class SnapshotRepository { + /** + * Stores the directory the snapshots come from. + */ + private File dir; + + /** + * Create a new instance of a repository. + * + * @param dir + */ + public SnapshotRepository(File dir) { + this.dir = dir; + } + + /** + * Create a new instance of a repository. + * + * @param dir + */ + public SnapshotRepository(String dir) { + this.dir = new File(dir); + } + + /** + * Get a list of snapshots in a directory. The newest snapshot is + * near the top of the array. + * + * @return + */ + public Snapshot[] getSnapshots() { + FilenameFilter filter = new FilenameFilter() { + public boolean accept(File dir, String name) { + File f = new File(dir, name); + return (name.toLowerCase().endsWith(".zip") + && f.isFile()) + || f.isDirectory(); + } + }; + + String[] snapshotNames = dir.list(filter); + + if (snapshotNames == null || snapshotNames.length == 0) { + return new Snapshot[0]; + } + + Snapshot[] snapshots = new Snapshot[snapshotNames.length]; + + Arrays.sort(snapshotNames, Collections.reverseOrder()); + + int i = 0; + for (String name : snapshotNames) { + snapshots[i] = new Snapshot(this, name); + i++; + } + + return snapshots; + } + + /** + * Get the default snapshot. + * + * @return + */ + public Snapshot getDefaultSnapshot() { + Snapshot[] snapshots = getSnapshots(); + + if (snapshots.length == 0) { + return null; + } + + return snapshots[0]; + } + + /** + * Check to see if a snapshot is valid. + * + * @param dir + * @param snapshot + * @return whether it is a valid snapshot + */ + public boolean isValidSnapshotName(String snapshot) { + if (!snapshot.matches("[A-Za-z0-9_\\-,.\\[\\]\\(\\) ]{1,50}")) { + return false; + } + + File f = new File(dir, snapshot); + return (f.isDirectory() && (new File(f, "level.dat")).exists()) + || (f.isFile() && f.getName().toLowerCase().endsWith((".zip"))); + } + + /** + * Get a snapshot. + * + * @param name + * @return + * @throws InvalidSnapshotException + */ + public Snapshot getSnapshot(String name) throws InvalidSnapshotException { + if (!isValidSnapshotName(name)) { + throw new InvalidSnapshotException(); + } + + return new Snapshot(this, name); + } + + /** + * Get the snapshot directory. + * + * @return + */ + public File getDirectory() { + return dir; + } +}