From 07816376177775ad79498968ef8da1376a56b1cc Mon Sep 17 00:00:00 2001 From: Luck Date: Thu, 23 Jun 2016 16:37:29 +0100 Subject: [PATCH] Add flatfile storage support --- README.creole | 4 +- .../me/lucko/luckperms/LPBukkitPlugin.java | 4 + bukkit/src/main/resources/config.yml | 2 +- .../me/lucko/luckperms/LPBungeePlugin.java | 4 + bungee/src/main/resources/config.yml | 2 +- .../data/methods/FlatfileDatastore.java | 425 ++++++++++++++++++ 6 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 common/src/main/java/me/lucko/luckperms/data/methods/FlatfileDatastore.java diff --git a/README.creole b/README.creole index b105df23..7da70e69 100644 --- a/README.creole +++ b/README.creole @@ -10,10 +10,10 @@ A (fairly bad) permissions implementation for Bukkit/BungeeCord. * **Easy and simple setup and configuration using commands** - no editing yml files, yuck * **Efficient/lightweight** - maybe? Who knows, it might be. * **BungeeCord compatible** - permissions, users and groups are synced across Bukkit/BungeeCord instances -* **Support for MySQL and SQLite** - other storage methods coming soon (maybe) +* **Support for MySQL, SQLite & Flatfile (JSON)** - other storage methods coming soon (maybe) ===== Possible Caveats -* Currently only supports MySQL and SQLite (support for more methods might come in the future) +* Currently only supports MySQL, SQLite & Flatfile (JSON) (support for more methods might come in the future) * Not at all tested and could produce unexpected/buggy results and errors === Setup diff --git a/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java b/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java index 2b757634..f52a1511 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java @@ -3,6 +3,7 @@ package me.lucko.luckperms; import lombok.Getter; import me.lucko.luckperms.data.Datastore; import me.lucko.luckperms.data.MySQLConfiguration; +import me.lucko.luckperms.data.methods.FlatfileDatastore; import me.lucko.luckperms.data.methods.MySQLDatastore; import me.lucko.luckperms.data.methods.SQLiteDatastore; import me.lucko.luckperms.groups.GroupManager; @@ -58,6 +59,9 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { } else if (storageMethod.equalsIgnoreCase("sqlite")) { getLogger().info("Using SQLite as storage method."); datastore = new SQLiteDatastore(this, new File(getDataFolder(), "luckperms.sqlite")); + } else if (storageMethod.equalsIgnoreCase("flatfile")) { + getLogger().info("Using Flatfile (JSON) as storage method."); + datastore = new FlatfileDatastore(this, getDataFolder()); } else { getLogger().warning("Storage method '" + storageMethod + "' was not recognised. Using SQLite as fallback."); datastore = new SQLiteDatastore(this, new File(getDataFolder(), "luckperms.sqlite")); diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index 901a8487..718ac0e4 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -12,7 +12,7 @@ include-global: true prefix: '&7&l[&b&lL&a&lP&7&l] &c' # Which storage method the plugin should use. -# Currently supported: mysql, sqlite +# Currently supported: mysql, sqlite, flatfile # Fill out connection info below if you're using MySQL storage-method: sqlite diff --git a/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java b/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java index a558b685..512cf8f2 100644 --- a/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java +++ b/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java @@ -4,6 +4,7 @@ import lombok.Getter; import me.lucko.luckperms.commands.CommandManager; import me.lucko.luckperms.data.Datastore; import me.lucko.luckperms.data.MySQLConfiguration; +import me.lucko.luckperms.data.methods.FlatfileDatastore; import me.lucko.luckperms.data.methods.MySQLDatastore; import me.lucko.luckperms.data.methods.SQLiteDatastore; import me.lucko.luckperms.groups.GroupManager; @@ -50,6 +51,9 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { } else if (storageMethod.equalsIgnoreCase("sqlite")) { getLogger().info("Using SQLite as storage method."); datastore = new SQLiteDatastore(this, new File(getDataFolder(), "luckperms.sqlite")); + } else if (storageMethod.equalsIgnoreCase("flatfile")) { + getLogger().info("Using Flatfile (JSON) as storage method."); + datastore = new FlatfileDatastore(this, getDataFolder()); } else { getLogger().warning("Storage method '" + storageMethod + "' was not recognised. Using SQLite as fallback."); datastore = new SQLiteDatastore(this, new File(getDataFolder(), "luckperms.sqlite")); diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 527b0b00..884bc44e 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -12,7 +12,7 @@ include-global: false prefix: '&7&l[&b&lL&a&lP&7&l] &c' # Which storage method the plugin should use. -# Currently supported: mysql, sqlite +# Currently supported: mysql, sqlite, flatfile # Fill out connection info below if you're using MySQL storage-method: sqlite diff --git a/common/src/main/java/me/lucko/luckperms/data/methods/FlatfileDatastore.java b/common/src/main/java/me/lucko/luckperms/data/methods/FlatfileDatastore.java new file mode 100644 index 00000000..38157d49 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/data/methods/FlatfileDatastore.java @@ -0,0 +1,425 @@ +package me.lucko.luckperms.data.methods; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import me.lucko.luckperms.LuckPermsPlugin; +import me.lucko.luckperms.data.Datastore; +import me.lucko.luckperms.exceptions.ObjectAlreadyHasException; +import me.lucko.luckperms.groups.Group; +import me.lucko.luckperms.users.User; + +import java.io.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@SuppressWarnings({"ResultOfMethodCallIgnored", "UnnecessaryLocalVariable"}) +public class FlatfileDatastore extends Datastore { + + private Map uuidCache = new ConcurrentHashMap<>(); + + private final File pluginDir; + private File usersDir; + private File groupsDir; + private File uuidData; + + public FlatfileDatastore(LuckPermsPlugin plugin, File pluginDir) { + super(plugin, "Flatfile - JSON"); + this.pluginDir = pluginDir; + } + + private boolean doWrite(File file, WriteOperation writeOperation) { + boolean success = false; + + FileWriter fileWriter = null; + BufferedWriter bufferedWriter = null; + JsonWriter jsonWriter = null; + + try { + fileWriter = new FileWriter(file); + bufferedWriter = new BufferedWriter(fileWriter); + jsonWriter = new JsonWriter(bufferedWriter); + jsonWriter.setIndent(" "); + success = writeOperation.onRun(jsonWriter); + } catch (IOException e) { + e.printStackTrace(); + } finally { + close(jsonWriter); + close(bufferedWriter); + close(fileWriter); + } + + return success; + } + + private boolean doRead(File file, ReadOperation readOperation) { + boolean success = false; + + FileReader fileReader = null; + BufferedReader bufferedReader = null; + JsonReader jsonReader = null; + + try { + fileReader = new FileReader(file); + bufferedReader = new BufferedReader(fileReader); + jsonReader = new JsonReader(bufferedReader); + success = readOperation.onRun(jsonReader); + } catch (IOException e) { + e.printStackTrace(); + } finally { + close(jsonReader); + close(bufferedReader); + close(fileReader); + } + + return success; + } + + private void close(AutoCloseable closeable) { + if (closeable == null) return; + try { + closeable.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void init() { + try { + makeFiles(); + } catch (IOException e) { + // TODO catch here or something + e.printStackTrace(); + return; + } + + uuidCache.putAll(getUUIDCache()); + } + + private void makeFiles() throws IOException { + File data = new File(pluginDir, "data"); + data.mkdirs(); + + usersDir = new File(data, "users"); + usersDir.mkdir(); + + groupsDir = new File(data, "groups"); + groupsDir.mkdir(); + + uuidData = new File(data, "uuidcache.txt"); + uuidData.createNewFile(); + } + + @Override + public void shutdown() { + saveUUIDCache(uuidCache); + } + + @Override + public boolean loadOrCreateUser(UUID uuid, String username) { + User user = plugin.getUserManager().makeUser(uuid, username); + try { + user.setPermission(plugin.getConfiguration().getDefaultGroupNode(), true); + } catch (ObjectAlreadyHasException ignored) {} + + File userFile = new File(usersDir, uuid.toString() + ".json"); + if (!userFile.exists()) { + try { + userFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + boolean success = doWrite(userFile, writer -> { + writer.beginObject(); + writer.name("uuid").value(user.getUuid().toString()); + writer.name("name").value(user.getName()); + writer.name("perms"); + writer.beginObject(); + for (Map.Entry e : user.getNodes().entrySet()) { + writer.name(e.getKey()).value(e.getValue().booleanValue()); + } + writer.endObject(); + writer.endObject(); + return true; + }); + + if (!success) return false; + } + + boolean success = doRead(userFile, reader -> { + reader.beginObject(); + reader.nextName(); // uuid record + reader.nextString(); // uuid + reader.nextName(); // name record + reader.nextString(); // name + reader.nextName(); //perms + reader.beginObject(); + while (reader.hasNext()) { + String node = reader.nextName(); + boolean b = reader.nextBoolean(); + user.getNodes().put(node, b); + } + + reader.endObject(); + reader.endObject(); + return true; + }); + + // User updating and loading should be done sync as permission attachments are updated + if (success) plugin.doSync(() -> plugin.getUserManager().updateOrSetUser(user)); + return success; + } + + @Override + public boolean loadUser(UUID uuid) { + User user = plugin.getUserManager().makeUser(uuid); + + File userFile = new File(usersDir, uuid.toString() + ".json"); + if (!userFile.exists()) { + return false; + } + + boolean success = doRead(userFile, reader -> { + reader.beginObject(); + reader.nextName(); // uuid record + reader.nextString(); // uuid + reader.nextName(); // name record + user.setName(reader.nextString()); // name + reader.nextName(); //perms + reader.beginObject(); + while (reader.hasNext()) { + String node = reader.nextName(); + boolean b = reader.nextBoolean(); + user.getNodes().put(node, b); + } + + reader.endObject(); + reader.endObject(); + return true; + }); + + // User updating and loading should be done sync as permission attachments are updated + if (success) plugin.doSync(() -> plugin.getUserManager().updateOrSetUser(user)); + return success; + } + + @Override + public boolean saveUser(User user) { + File userFile = new File(usersDir, user.getUuid().toString() + ".json"); + if (!userFile.exists()) { + try { + userFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + boolean success = doWrite(userFile, writer -> { + writer.beginObject(); + writer.name("uuid").value(user.getUuid().toString()); + writer.name("name").value(user.getName()); + writer.name("perms"); + writer.beginObject(); + for (Map.Entry e : user.getNodes().entrySet()) { + writer.name(e.getKey()).value(e.getValue().booleanValue()); + } + writer.endObject(); + writer.endObject(); + return true; + }); + return success; + } + + @Override + public boolean createAndLoadGroup(String name) { + Group group = plugin.getGroupManager().makeGroup(name); + + File groupFile = new File(groupsDir, name + ".json"); + if (!groupFile.exists()) { + try { + groupFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + boolean success = doWrite(groupFile, writer -> { + writer.beginObject(); + writer.name("name").value(group.getName()); + writer.name("perms"); + writer.beginObject(); + for (Map.Entry e : group.getNodes().entrySet()) { + writer.name(e.getKey()).value(e.getValue().booleanValue()); + } + writer.endObject(); + writer.endObject(); + return true; + }); + + if (!success) return false; + } + + boolean success = doRead(groupFile, reader -> { + reader.beginObject(); + reader.nextName(); // name record + reader.nextString(); // name + reader.nextName(); //perms + reader.beginObject(); + while (reader.hasNext()) { + String node = reader.nextName(); + boolean b = reader.nextBoolean(); + group.getNodes().put(node, b); + } + + reader.endObject(); + reader.endObject(); + return true; + }); + + if (success) plugin.getGroupManager().updateOrSetGroup(group); + return success; + } + + @Override + public boolean loadGroup(String name) { + Group group = plugin.getGroupManager().makeGroup(name); + + File groupFile = new File(groupsDir, name + ".json"); + if (!groupFile.exists()) { + return false; + } + + boolean success = doRead(groupFile, reader -> { + reader.beginObject(); + reader.nextName(); // name record + reader.nextString(); // name + reader.nextName(); //perms + reader.beginObject(); + while (reader.hasNext()) { + String node = reader.nextName(); + boolean b = reader.nextBoolean(); + group.getNodes().put(node, b); + } + + reader.endObject(); + reader.endObject(); + return true; + }); + + if (success) plugin.getGroupManager().updateOrSetGroup(group); + return success; + } + + @Override + public boolean loadAllGroups() { + List groups = Arrays.asList(groupsDir.list((dir, name1) -> name1.endsWith(".json"))); + groups.forEach(this::loadGroup); + return true; + } + + @Override + public boolean saveGroup(Group group) { + File groupFile = new File(groupsDir, group.getName() + ".json"); + if (!groupFile.exists()) { + try { + groupFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + boolean success = doWrite(groupFile, writer -> { + writer.beginObject(); + writer.name("name").value(group.getName()); + writer.name("perms"); + writer.beginObject(); + for (Map.Entry e : group.getNodes().entrySet()) { + writer.name(e.getKey()).value(e.getValue().booleanValue()); + } + writer.endObject(); + writer.endObject(); + return true; + }); + + return success; + } + + @Override + public boolean deleteGroup(Group group) { + File groupFile = new File(groupsDir, group.getName() + ".json"); + if (groupFile.exists()) { + groupFile.delete(); + } + return true; + } + + private Map getUUIDCache() { + Map cache = new HashMap<>(); + + FileReader fileReader = null; + BufferedReader bufferedReader = null; + + try { + fileReader = new FileReader(uuidData); + bufferedReader = new BufferedReader(fileReader); + + Properties props = new Properties(); + props.load(bufferedReader); + for (String key : props.stringPropertyNames()) { + cache.put(key, props.getProperty(key)); + } + + } catch (IOException e) { + e.printStackTrace(); + } finally { + close(bufferedReader); + close(fileReader); + } + + return cache; + } + + private void saveUUIDCache(Map cache) { + FileWriter fileWriter = null; + BufferedWriter bufferedWriter = null; + + try { + fileWriter = new FileWriter(uuidData); + bufferedWriter = new BufferedWriter(fileWriter); + + Properties properties = new Properties(); + properties.putAll(cache); + properties.store(bufferedWriter, null); + + } catch (IOException e) { + e.printStackTrace(); + } finally { + close(bufferedWriter); + close(fileWriter); + } + } + + @Override + public boolean saveUUIDData(String username, UUID uuid) { + uuidCache.put(username, uuid.toString()); + return true; + } + + @Override + public UUID getUUID(String username) { + if (uuidCache.get(username) == null) return null; + return UUID.fromString(uuidCache.get(username)); + } + + interface WriteOperation { + boolean onRun(JsonWriter writer) throws IOException; + } + + interface ReadOperation { + boolean onRun(JsonReader reader) throws IOException; + } +}