diff --git a/app/build.gradle b/app/build.gradle index 2a11f36..5345a80 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,6 +22,8 @@ dependencies { // This dependency is used by the application. implementation 'com.google.guava:guava:30.1-jre' + // Use gson to serialize/deserialize json files + implementation 'com.google.code.gson:gson:2.9.0' } application { diff --git a/app/src/main/java/lightcontainer/storage/Adapter.java b/app/src/main/java/lightcontainer/storage/Adapter.java new file mode 100644 index 0000000..29d5dc3 --- /dev/null +++ b/app/src/main/java/lightcontainer/storage/Adapter.java @@ -0,0 +1,8 @@ +package lightcontainer.storage; + +public interface Adapter { + + String toString(); + + AppData fromString(String appDataString); +} diff --git a/app/src/main/java/lightcontainer/storage/AppConfig.java b/app/src/main/java/lightcontainer/storage/AppConfig.java new file mode 100644 index 0000000..7f767b8 --- /dev/null +++ b/app/src/main/java/lightcontainer/storage/AppConfig.java @@ -0,0 +1,88 @@ +package lightcontainer.storage; + +/** + * AppConfig represents all network related information needed for the program to work. + * + * @author Maximilien LEDOUX + * @version 1.0 + * @since 1.0 + */ +public class AppConfig { + + private static AppConfig instance = null; + private int unicastPort; + private String multicastIp; + private int multicastPort; + private String networkInterface; + private boolean isTls; + + /** + * Constructs a new instance of AppConfig. + * Sets all data to default values. + */ + private AppConfig() { + this.unicastPort = -1; + this.multicastIp = "NONE"; + this.multicastPort = -1; + this.networkInterface = "NONE"; + this.isTls = false; + } + + /** + * @return An instance of this class. Always returns the same instance. + */ + public static AppConfig getInstance() { + if (instance == null) { + instance = new AppConfig(); + } + return instance; + } + + public int getUnicastPort() { + return unicastPort; + } + + public void setUnicastPort(int unicastPort) { + if (this.unicastPort == -1) { + this.unicastPort = unicastPort; + } + } + + public String getMulticastIp() { + return multicastIp; + } + + public void setMulticastIp(String multicastIp) { + if (this.multicastIp.equals("NONE")) { + this.multicastIp = multicastIp; + } + } + + public int getMulticastPort() { + return multicastPort; + } + + public void setMulticastPort(int multicastPort) { + if (this.multicastPort == -1) { + this.multicastPort = multicastPort; + } + } + + public String getNetworkInterface() { + return networkInterface; + } + + public void setNetworkInterface(String networkInterface) { + if (this.networkInterface.equals("NONE")) { + this.networkInterface = networkInterface; + } + } + + public boolean isTls() { + return isTls; + } + + public void setTls(boolean tls) { + this.isTls = tls; + } +} diff --git a/app/src/main/java/lightcontainer/storage/AppData.java b/app/src/main/java/lightcontainer/storage/AppData.java new file mode 100644 index 0000000..2a693b2 --- /dev/null +++ b/app/src/main/java/lightcontainer/storage/AppData.java @@ -0,0 +1,152 @@ +package lightcontainer.storage; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * AppData represents the database of the FileFrontEnd program. + * It contains an AppConfig instance and a collection of Users. + * + * @author Maximilien LEDOUX + * @version 1.0 + * @see AppConfig + * @see User + * @since 1.0 + */ +public class AppData { + + private static AppData instance = null; + private AppConfig appConfig; + private final Map users; + + + /** + * Constructs a new instance of AppData. + * Sets appConfig to null and creates a new Hashmap of users. + */ + private AppData() { + this.appConfig = null; + this.users = new HashMap<>(); + } + + /** + * @return An instance of this class. Always returns the same instance. + */ + public static AppData getInstance() { + if (instance == null) { + instance = new AppData(); + } + return instance; + } + + /** + * @return The AppConfig + */ + public AppConfig getAppConfig() { + return appConfig; + } + + /** + * Sets the AppConfig. This method sets the AppConfig for once and for all. + * It is locked after first call. + * + * @param appConfig The network configuration of the program. + */ + public void setAppConfig(AppConfig appConfig) { + if (this.appConfig == null) { + this.appConfig = appConfig; + } + } + + /** + * @param userName The name of the user. + * @return The user corresponding to userName, null otherwise. + */ + public User getUser(String userName) { + return this.users.get(userName); + } + + /** + * Use this method when a user signs up. + * + * @param user The user to add. + * @return True if the user was added. False if a user with the same name already exists. + */ + public boolean addUser(User user) { + if (this.users.containsKey(user.getName())) { + return false; + } else { + this.users.put(user.getName(), user); + return true; + } + } + + public Iterator usersIterator() { + return users.values().iterator(); + } + + /** + * @param fileName The name of the file + * @param user The user + * @return The file corresponding to the given name and belonging to the user. Null if the user cannot be found or the file cannot be found + * @deprecated Maybe not useful. DO NOT USE FOR THE TIME BEING + */ + public File getFileOf(String fileName, User user) { + return this.users.get(user.getName()).getFile(fileName); + } + + /** + * Call this method after receiving SAVEFILE_OK from the StorBackEnd. + * Do NOT call when receiving SAVEFILE_ERROR, or it will break the system's synchronization. + *

+ * Adds the file of for a specific user. + * True indicates the success of the operation. + * False indicates the failure of the operation. + * + * @param file The file to add + * @param user The user who wants to add the file + * @return True if the user is found and a file with the same name doesn't already exist for this user. False otherwise. + */ + public boolean addFileFor(File file, User user) { + if (!this.users.containsKey(user.getName())) { + return false; + } else { + this.users.get(user.getName()).addFile(file); + return true; + } + } + + /** + * Call this method after receiving REMOVEFILE_OK from the StorBackEnd. + * Do NOT call when receiving REMOVEFILE_ERROR, or it will break the system's synchronization. + * Deletes the file of for a specific user. + * True indicates the success of the operation. + * False indicates the failure of the operation. + * + * @param fileName The name of the file to delete + * @param user The user who wants to delete the file + * @return True if the user is found and the file was deleted. False otherwise. + */ + public boolean deleteFileOf(String fileName, User user) { + if (!this.users.containsKey(user.getName())) { + return false; + } else { + return this.users.get(user.getName()).deleteFile(fileName); + } + } + + /** + * @param user The user who wants to add a storage for their file + * @param file The file that needs a new storage + * @param storage The storage to add + * @return True if the storage was added. False otherwise. + */ + public boolean addStorage(User user, File file, String storage) { + if (!this.users.containsKey(user.getName())) { + return false; + } else { + return this.users.get(user.getName()).addStorage(file, storage); + } + } +} diff --git a/app/src/main/java/lightcontainer/storage/File.java b/app/src/main/java/lightcontainer/storage/File.java new file mode 100644 index 0000000..e7d5d5f --- /dev/null +++ b/app/src/main/java/lightcontainer/storage/File.java @@ -0,0 +1,47 @@ +package lightcontainer.storage; + +import java.util.Iterator; +import java.util.Set; + +/** + * File represents all information related to a file + */ +public class File { + + private final String name; + private final int size; + private final String iv; + private final Set storage; + + public File(String name, int size, String iv, Set storage) { + this.name = name; + this.size = size; + this.iv = iv; + this.storage = storage; + } + + public String getName() { + return name; + } + + public int getSize() { + return size; + } + + public String getIv() { + return iv; + } + + public Iterator getStorageIterator() { + return storage.iterator(); + } + + public boolean addStorage(String storage) { + if (this.storage.contains(storage)) { + return false; + } else { + this.storage.add(storage); + return true; + } + } +} diff --git a/app/src/main/java/lightcontainer/storage/JsonAdapter.java b/app/src/main/java/lightcontainer/storage/JsonAdapter.java new file mode 100644 index 0000000..aaecc55 --- /dev/null +++ b/app/src/main/java/lightcontainer/storage/JsonAdapter.java @@ -0,0 +1,145 @@ +package lightcontainer.storage; + +import com.google.gson.*; + +import java.util.*; + +/** + * Specific implementation of Adapter that converts AppData to Json and vice-versa + */ +public class JsonAdapter implements Adapter { + + private AppData appData; + + public JsonAdapter(AppData appData) { + this.appData = appData; + } + + /** + * + * @return A Json String containing AppData properties + */ + @Override + public String toString() { + return addData(appData); + } + + private String addData(AppData appData) { + AppConfig appConfig = appData.getAppConfig(); + JsonObject config = new JsonObject(); + config.addProperty("unicast_port", appConfig.getUnicastPort()); + config.addProperty("multicast_ip", appConfig.getMulticastIp()); + config.addProperty("multicast_port", appConfig.getMulticastPort()); + config.addProperty("network_interface", appConfig.getNetworkInterface()); + config.addProperty("tls", appConfig.isTls()); + JsonArray users = new JsonArray(); + Iterator userIterator = appData.usersIterator(); + addUsers(users, userIterator); + config.add("users", users); + return config.toString(); + } + + private void addUsers(JsonArray users, Iterator userIterator) { + while (userIterator.hasNext()) { + User current = userIterator.next(); + JsonObject user = new JsonObject(); + user.addProperty("name", current.getName()); + user.addProperty("password", current.getPassword()); + user.addProperty("aes_key", current.getAesKey()); + JsonArray files = new JsonArray(); + Iterator fileIterator = current.fileIterator(); + addFiles(fileIterator, files); + user.add("files", files); + users.add(user); + } + } + + private void addFiles(Iterator fileIterator, JsonArray files) { + while (fileIterator.hasNext()) { + File currentFile = fileIterator.next(); + JsonObject file = new JsonObject(); + file.addProperty("name", currentFile.getName()); + file.addProperty("size", currentFile.getSize()); + file.addProperty("iv", currentFile.getIv()); + JsonArray storage = new JsonArray(); + Iterator storageIterator = currentFile.getStorageIterator(); + addStorage(storage, storageIterator); + file.add("storage", storage); + files.add(file); + } + } + + private void addStorage(JsonArray storage, Iterator storageIterator) { + while (storageIterator.hasNext()) { + String storageString = storageIterator.next(); + storage.add(storageString); + } + } + + /** + * + * @param appDataString The Json String to convert + * @return An AppData instance + */ + @Override + public AppData fromString(String appDataString) { + try { + JsonElement jsonString = JsonParser.parseString(appDataString); + JsonObject jsonAppData = jsonString.getAsJsonObject(); + AppConfig appConfig = AppConfig.getInstance(); + appConfig.setUnicastPort(jsonAppData.get("unicast_port").getAsInt()); + appConfig.setMulticastIp(jsonAppData.get("multicast_ip").getAsString()); + appConfig.setMulticastPort(jsonAppData.get("multicast_port").getAsInt()); + appConfig.setNetworkInterface(jsonAppData.get("network_interface").getAsString()); + appConfig.setTls(jsonAppData.get("tls").getAsBoolean()); + JsonArray jsonUsers = jsonAppData.getAsJsonArray("users"); + List users = new ArrayList<>(); + getUsers(jsonUsers, users); + AppData appData = AppData.getInstance(); + appData.setAppConfig(appConfig); + for (User user : users) { + appData.addUser(user); + } + this.appData = appData; + return this.appData; + } catch (JsonParseException parseException) { + System.out.println("[FFE] : Error while loading configuration file"); //TODO - changer en log + return null; + } + } + + private void getUsers(JsonArray jsonUsers, List users) { + for (JsonElement element : jsonUsers) { + JsonObject jsonUser = element.getAsJsonObject(); + String name = jsonUser.get("name").getAsString(); + String password = jsonUser.get("password").getAsString(); + String aeskey = jsonUser.get("aes_key").getAsString(); + Map userFiles = new HashMap<>(); + JsonArray jsonFiles = jsonUser.getAsJsonArray("files"); + getFiles(userFiles, jsonFiles); + User user = new User(name, password, aeskey, userFiles); + users.add(user); + } + } + + private void getFiles(Map userFiles, JsonArray jsonFiles) { + for (JsonElement fileElement : jsonFiles) { + JsonObject jsonFile = fileElement.getAsJsonObject(); + String fileName = jsonFile.get("name").getAsString(); + int size = jsonFile.get("size").getAsInt(); + String iv = jsonFile.get("iv").getAsString(); + Set storage = new HashSet<>(); + JsonArray jsonStorage = jsonFile.getAsJsonArray("storage"); + getStorage(storage, jsonStorage); + File file = new File(fileName, size, iv, storage); + userFiles.put(file.getName(), file); + } + } + + private void getStorage(Set storage, JsonArray jsonStorage) { + for (JsonElement storageElement : jsonStorage) { + String storageName = storageElement.getAsString(); + storage.add(storageName); + } + } +} diff --git a/app/src/main/java/lightcontainer/storage/Repository.java b/app/src/main/java/lightcontainer/storage/Repository.java new file mode 100644 index 0000000..1f085a6 --- /dev/null +++ b/app/src/main/java/lightcontainer/storage/Repository.java @@ -0,0 +1,51 @@ +package lightcontainer.storage; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +public class Repository { + + /** + * @param filePath The path where the file must be saved + * @param adapter The service that converts Objects to Strings + */ + static void save(String filePath, Adapter adapter) { + if (filePath != null) { + String jsonAppData = adapter.toString(); + try (BufferedWriter bufferedWriter = Files.newBufferedWriter(Paths.get(filePath).toAbsolutePath(), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + bufferedWriter.write(jsonAppData); + bufferedWriter.flush(); + } catch (IOException e) { + System.out.println("Error while saving configuration file !"); + } + } + } + + /** + * @param filePath The path where the file is stored + * @param adapter The service that converts Strings to objects + * @return + */ + static AppData load(String filePath, Adapter adapter) { + String jsonString = readFile(filePath); + return adapter.fromString(jsonString); + } + + private static String readFile(String filePath) { + StringBuilder builder = new StringBuilder(); + try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath).toAbsolutePath(), StandardCharsets.UTF_8)) { + while (reader.ready()) { + builder.append(reader.readLine()); + } + } catch (IOException e) { + System.out.println("Error while reading configuration file"); + builder.setLength(0); + } + return builder.toString(); + } +} diff --git a/app/src/main/java/lightcontainer/storage/User.java b/app/src/main/java/lightcontainer/storage/User.java new file mode 100644 index 0000000..47a65f9 --- /dev/null +++ b/app/src/main/java/lightcontainer/storage/User.java @@ -0,0 +1,80 @@ +package lightcontainer.storage; + +import java.util.Iterator; +import java.util.Map; + +/** + * User represents a user of the system. + * + * @author Maximilien LEDOUX + * @version 1.0 + * @since 1.0 + */ +public class User { + + private final String Name; + private final String password; + private final String aesKey; + private final Map files; + + public User(String Name, String password, String aesKey, Map files) { + this.Name = Name; + this.password = password; + this.aesKey = aesKey; + this.files = files; + } + + public String getName() { + return Name; + } + + public String getPassword() { + return password; + } + + public String getAesKey() { + return aesKey; + } + + public Iterator fileIterator() { + return files.values().iterator(); + } + + public File getFile(String fileName) { + return this.files.get(fileName); + } + + /** + * @param file The file to add. + * @return False if a file with the same name already exists. Otherwise, adds the file and returns true. + */ + public void addFile(File file) { + this.files.put(file.getName(), file); + } + + /** + * @param fileName The name of the file to delete. + * @return True if the file was deleted. False otherwise. + */ + public boolean deleteFile(String fileName) { + if (this.files.containsKey(fileName)) { + this.files.remove(fileName); + return true; + } else { + return false; + } + } + + /** + * @param file The file that needs a storage + * @param storage The storage name + * @return True if the storage was added to the file. False otherwise. + */ + public boolean addStorage(File file, String storage) { + if (this.files.containsKey(file.getName())) { + return file.addStorage(storage); + } else { + return false; + } + } +} diff --git a/app/src/main/resources/rules.txt b/app/src/main/resources/rules.txt index 5c800fd..8be9487 100644 --- a/app/src/main/resources/rules.txt +++ b/app/src/main/resources/rules.txt @@ -33,7 +33,7 @@ client_signin = ^SIGNIN ([A-Za-z0-9]{2,20}) ([^ !]{5,50})\r\n$ client_signup = ^SIGNUP ([A-Za-z0-9]{2,20}) ([^ !]{5,50})\r\n$ ffe_signresult = ^(SIGN_OK|SIGN_ERROR)\r\n$ client_filelist = ^FILELIST\r\n$ -ffe_filelistresult = ^FILES (([^ !]{1,20})!([0-9]{1,10})){0,50}$ +ffe_filelistresult = ^FILES( ([^ !]{1,20})!([0-9]{1,10})){0,50}$ client_savefile = ^SAVE_FILE ([^ !]{1,20}) ([0-9]{1,10})\r\n$ ffe_savefileresult = ^(SAVEFILE_OK|SAVEFILE_ERROR)\r\n$ client_getfile = ^GETFILE ([^ !]{1,20})\r\n$ diff --git a/app/src/test/java/lightcontainer/protocol/rules/reader/HelloRuleTest.java b/app/src/test/java/lightcontainer/protocol/rules/reader/HelloRuleTest.java index 7b82f38..a356fb5 100644 --- a/app/src/test/java/lightcontainer/protocol/rules/reader/HelloRuleTest.java +++ b/app/src/test/java/lightcontainer/protocol/rules/reader/HelloRuleTest.java @@ -1,7 +1,6 @@ package lightcontainer.protocol.rules.reader; import lightcontainer.protocol.ProtocolReader; -import lightcontainer.protocol.rules.reader.HelloRule; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; diff --git a/app/src/test/java/lightcontainer/storage/JsonAdapterTests.java b/app/src/test/java/lightcontainer/storage/JsonAdapterTests.java new file mode 100644 index 0000000..bd9006a --- /dev/null +++ b/app/src/test/java/lightcontainer/storage/JsonAdapterTests.java @@ -0,0 +1,66 @@ +package lightcontainer.storage; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + + +public class JsonAdapterTests { + + @Test + public void convertAppDataToJson() { + //GIVEN an AppData instance and a Json Adapter + AppData appData = AppData.getInstance(); + AppConfig appConfig = AppConfig.getInstance(); + appConfig.setUnicastPort(32000); + appConfig.setMulticastIp("224.25.0.1"); + appConfig.setMulticastPort(15502); + appConfig.setNetworkInterface("My network interface"); + appConfig.setTls(false); + Map files = new HashMap<>(); + Set storage = new HashSet<>(); + storage.add("StorBackEnd1"); + File file1 = new File("File1", 15, "8d8d8d8d", storage); + files.put(file1.getName(), file1); + User user1 = new User("User1", "Password", "djdjjdj", files); + appData.setAppConfig(appConfig); + appData.addUser(user1); + JsonAdapter jsonAdapter = new JsonAdapter(appData); + //WHEN the adapter converts AppData to Json + String jsonAppData = jsonAdapter.toString(); + //THEN + assertTrue(jsonAppData.contains("32000")); + assertTrue(jsonAppData.contains("224.25.0.1")); + assertTrue(jsonAppData.contains("15502")); + assertTrue(jsonAppData.contains("My network interface")); + assertTrue(jsonAppData.contains("false")); + assertTrue(jsonAppData.contains("User1")); + assertTrue(jsonAppData.contains("Password")); + assertTrue(jsonAppData.contains("djdjjdj")); + assertTrue(jsonAppData.contains("File1")); + assertTrue(jsonAppData.contains("15")); + assertTrue(jsonAppData.contains("8d8d8d8d")); + assertTrue(jsonAppData.contains("StorBackEnd1")); + } + + @Test + public void convertJsonToAppData() { + //GIVEN a Json string + String json = "{\"unicast_port\":32000,\"multicast_ip\":\"224.25.0.1\",\"multicast_port\":15502,\"network_interface\":\"My network interface\",\"tls\":false,\"users\":[{\"name\":\"User1\",\"password\":\"Password\",\"aes_key\":\"djdjjdj\",\"files\":[{\"name\":\"File1\",\"size\":15,\"iv\":\"8d8d8d8d\",\"storage\":[\"StorBackEnd1\"]}]}]}"; + //WHEN the adapter converts Json to Appdata + JsonAdapter jsonAdapter = new JsonAdapter(null); + AppData appData = jsonAdapter.fromString(json); + //THEN + assertNotNull(appData.getAppConfig()); + assertEquals("My network interface", appData.getAppConfig().getNetworkInterface()); + assertEquals(32000, appData.getAppConfig().getUnicastPort()); + assertEquals("224.25.0.1", appData.getAppConfig().getMulticastIp()); + assertEquals(15502, appData.getAppConfig().getMulticastPort()); + assertFalse(appData.getAppConfig().isTls()); + } +} diff --git a/app/src/test/java/lightcontainer/storage/RepositoryTests.java b/app/src/test/java/lightcontainer/storage/RepositoryTests.java new file mode 100644 index 0000000..ed90a68 --- /dev/null +++ b/app/src/test/java/lightcontainer/storage/RepositoryTests.java @@ -0,0 +1,71 @@ +package lightcontainer.storage; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + + +public class RepositoryTests { + + @AfterEach + public void destroyTestFile() { + try { + Files.deleteIfExists(Paths.get("src", "test", "resources", "test.json").toAbsolutePath()); + } catch (IOException e) { + System.out.println("Error while destroying file"); + } + } + + @Test + public void save() { + //GIVEN an AppData instance and a Json Adapter + AppData appData = AppData.getInstance(); + AppConfig appConfig = AppConfig.getInstance(); + appConfig.setUnicastPort(32000); + appConfig.setMulticastIp("224.25.0.1"); + appConfig.setMulticastPort(15502); + appConfig.setNetworkInterface("My network interface"); + appConfig.setTls(false); + Map files = new HashMap<>(); + Set storage = new HashSet<>(); + storage.add("StorBackEnd1"); + File file1 = new File("File1", 15, "8d8d8d8d", storage); + files.put(file1.getName(), file1); + User user1 = new User("User1", "Password", "djdjjdj", files); + appData.setAppConfig(appConfig); + appData.addUser(user1); + JsonAdapter jsonAdapter = new JsonAdapter(appData); + //WHEN Repository calls save method + String filePath = "src/test/resources/test.json"; + Repository.save(filePath, jsonAdapter); + //THEN + assertTrue(Files.exists(Paths.get("src/test/resources/test.json").toAbsolutePath())); + } + + @Test + public void load() { + //GIVEN a test json file loadTest.json + JsonAdapter jsonAdapter = new JsonAdapter(null); + //WHEN repository calls load method + AppData appData = Repository.load("src/test/resources/loadTest.json", jsonAdapter); + //THEN + assertNotNull(appData.getAppConfig()); + assertEquals("My network interface", appData.getAppConfig().getNetworkInterface()); + assertEquals(32000, appData.getAppConfig().getUnicastPort()); + assertEquals("224.25.0.1", appData.getAppConfig().getMulticastIp()); + assertEquals(15502, appData.getAppConfig().getMulticastPort()); + assertFalse(appData.getAppConfig().isTls()); + } +} diff --git a/app/src/test/resources/loadTest.json b/app/src/test/resources/loadTest.json new file mode 100644 index 0000000..f06f3af --- /dev/null +++ b/app/src/test/resources/loadTest.json @@ -0,0 +1,24 @@ +{ + "unicast_port": 32000, + "multicast_ip": "224.25.0.1", + "multicast_port": 15502, + "network_interface": "My network interface", + "tls": false, + "users": [ + { + "name": "User1", + "password": "Password", + "aes_key": "djdjjdj", + "files": [ + { + "name": "File1", + "size": 15, + "iv": "8d8d8d8d", + "storage": [ + "StorBackEnd1" + ] + } + ] + } + ] +} \ No newline at end of file