Repository: Cog-Creators/Red-DiscordBot

Issue:
[V3 Data management] We need a way to convert data from JSON to mongoDB, and revert
### Type:

- [x] Suggestion
- [ ] Bug

### Brief description of the problem

Right now, we can't switch from JSON to mongoDB (or in the other way), if we do, we lose all data. Creating a data convertor wouldn't be really hard but that'll be really helpful (especially with those glitched JSONs #1415 )


Discussion:

Resolving patch:
diff --git a/redbot/setup.py b/redbot/setup.py
--- a/redbot/setup.py
+++ b/redbot/setup.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 
 import argparse
+import asyncio
+import json
 import os
 import shutil
 import sys
@@ -40,6 +42,11 @@ def parse_cli_args():
         help="Interactively delete an instance",
         action="store_true"
     )
+    parser.add_argument(
+        "--edit", "-e",
+        help="Interactively edit an instance",
+        action="store_true"
+    )
     return parser.parse_known_args()
 
 
@@ -59,12 +66,7 @@ def save_config(name, data, remove=False):
     JsonIO(config_file)._save_json(config)
 
 
-def basic_setup():
-    """
-    Creates the data storage folder.
-    :return:
-    """
-
+def get_data_dir():
     default_data_dir = Path(appdir.user_data_dir)
 
     print("Hello! Before we begin the full configuration process we need to"
@@ -96,10 +98,10 @@ def basic_setup():
     if not confirm("Please confirm (y/n):"):
         print("Please start the process over.")
         sys.exit(0)
+    return default_data_dir
 
-    default_dirs = deepcopy(basic_config_default)
-    default_dirs['DATA_PATH'] = str(default_data_dir.resolve())
 
+def get_storage_type():
     storage_dict = {
         1: "JSON",
         2: "MongoDB"
@@ -118,15 +120,10 @@ def basic_setup():
         else:
             if storage not in storage_dict:
                 storage = None
+    return storage
 
-    default_dirs['STORAGE_TYPE'] = storage_dict.get(storage, 1)
-
-    if storage_dict.get(storage, 1) == "MongoDB":
-        from redbot.core.drivers.red_mongo import get_config_details
-        default_dirs['STORAGE_DETAILS'] = get_config_details()
-    else:
-        default_dirs['STORAGE_DETAILS'] = {}
 
+def get_name():
     name = ""
     while len(name) == 0:
         print()
@@ -135,7 +132,35 @@ def basic_setup():
         name = input("> ")
         if " " in name:
             name = ""
+    return name
+
+
+def basic_setup():
+    """
+    Creates the data storage folder.
+    :return:
+    """
+
+    default_data_dir = get_data_dir()
+
+    default_dirs = deepcopy(basic_config_default)
+    default_dirs['DATA_PATH'] = str(default_data_dir.resolve())
+
+    storage = get_storage_type()
+
+    storage_dict = {
+        1: "JSON",
+        2: "MongoDB"
+    }
+    default_dirs['STORAGE_TYPE'] = storage_dict.get(storage, 1)
+
+    if storage_dict.get(storage, 1) == "MongoDB":
+        from redbot.core.drivers.red_mongo import get_config_details
+        default_dirs['STORAGE_DETAILS'] = get_config_details()
+    else:
+        default_dirs['STORAGE_DETAILS'] = {}
 
+    name = get_name()
     save_config(name, default_dirs)
 
     print()
@@ -143,6 +168,116 @@ def basic_setup():
           " continue your setup process and to run the bot.")
 
 
+async def edit_instance():
+    instance_list = load_existing_config()
+    if not instance_list:
+        print("No instances have been set up!")
+        return
+
+    print(
+        "You have chosen to edit an instance. The following "
+        "is a list of instances that currently exist:\n"
+    )
+    for instance in instance_list.keys():
+        print("{}\n".format(instance))
+    print("Please select one of the above by entering its name")
+    selected = input("> ")
+
+    if selected not in instance_list.keys():
+        print("That isn't a valid instance!")
+        return
+    instance_data = instance_list[selected]
+    default_dirs = deepcopy(basic_config_default)
+
+    current_data_dir = Path(instance_data["DATA_PATH"])
+    print(
+        "You have selected '{}' as the instance to modify.".format(selected)
+    )
+    if not confirm("Please confirm (y/n):"):
+        print("Ok, we will not continue then.")
+        return
+
+    print("Ok, we will continue on.")
+    print()
+    if confirm("Would you like to change the instance name? (y/n)"):
+        name = get_name()
+    else:
+        name = selected
+
+    if confirm("Would you like to change the data location? (y/n)"):
+        default_data_dir = get_data_dir()
+        default_dirs["DATA_PATH"] = str(default_data_dir.resolve())
+    else:
+        default_dirs["DATA_PATH"] = str(current_data_dir.resolve())
+
+    if confirm("Would you like to change the storage type? (y/n):"):
+        storage = get_storage_type()
+
+        storage_dict = {
+            1: "JSON",
+            2: "MongoDB"
+        }
+        default_dirs["STORAGE_TYPE"] = storage_dict[storage]
+        if storage_dict.get(storage, 1) == "MongoDB":
+            from redbot.core.drivers.red_mongo import get_config_details, Mongo
+            storage_details = get_config_details()
+            default_dirs["STORAGE_DETAILS"] = storage_details
+
+            if instance_data["STORAGE_TYPE"] == "JSON":
+                if confirm("Would you like to import your data? (y/n)"):
+                    core_data_file = list(current_data_dir.glob("core/settings.json"))[0]
+                    m = Mongo("Core", **storage_details)
+                    with core_data_file.open(mode="r") as f:
+                        core_data = json.loads(f.read())
+                    m.unique_cog_identifier = 0
+                    collection = m.get_collection()
+                    await collection.update_one(
+                        {'_id': m.unique_cog_identifier},
+                        update={"$set": core_data["0"]},
+                        upsert=True
+                    )
+                    for p in current_data_dir.glob("cogs/**/settings.json"):
+                        cog_m = Mongo(p.parent.stem, **storage_details)
+                        cog_c = cog_m.get_collection()
+                        with p.open(mode="r") as f:
+                            cog_data = json.loads(f.read())
+                        for ident in list(cog_data.keys()):
+                            cog_m.unique_cog_identifier = int(ident)
+                            await cog_c.update_one(
+                                {"_id": cog_m.unique_cog_identifier},
+                                update={"$set": cog_data[ident]},
+                                upsert=True
+                            )
+        else:
+            default_dirs["STORAGE_DETAILS"] = {}
+            if instance_data["STORAGE_TYPE"] == "MongoDB":
+                from redbot.core.drivers.red_mongo import Mongo
+                from redbot.core.drivers.red_json import JSON
+                m = Mongo("Core", **instance_data["STORAGE_DETAILS"])
+                db = m.db
+                collection_names = await db.collection_names(include_system_collections=False)
+                for c_name in collection_names:
+                    if c_name == "Core":
+                        c_data_path = current_data_dir / "core"
+                    else:
+                        c_data_path = current_data_dir / "cogs/{}".format(c_name)
+                    output = {}
+                    docs = await db[c_name].find().to_list(None)
+                    for item in docs:
+                        item_id = str(item.pop("_id"))
+                        output[item_id] = item
+                    target = JSON(c_name, data_path_override=c_data_path)
+                    await target.jsonIO._threadsafe_save_json(output)
+
+    if name != selected:
+        save_config(selected, {}, remove=True)
+    save_config(name, default_dirs)
+
+    print(
+        "Your basic configuration has been edited"
+    )
+
+
 def remove_instance():
     instance_list = load_existing_config()
     if not instance_list:
@@ -162,12 +297,8 @@ def remove_instance():
         print("That isn't a valid instance!")
         return
     instance_data = instance_list[selected]
-    print(
-        "Would you like to make a backup of "
-        "the data for this instance (y/n)?"
-    )
-    yesno = input("> ")
-    if yesno.lower() == "y":
+
+    if confirm("Would you like to make a backup of the data for this instance? (y/n)"):
         if instance_data["STORAGE_TYPE"] == "MongoDB":
             raise NotImplementedError(
                 "Support for removing instances with MongoDB as the storage "
@@ -197,7 +328,7 @@ def remove_instance():
             save_config(selected, {}, remove=True)
             print("The instance has been removed")
             return
-    elif yesno.lower() == "n":
+    else:
         pth = Path(instance_data["DATA_PATH"])
         print("Removing the instance...")
         try:
@@ -207,9 +338,6 @@ def remove_instance():
         save_config(selected, {}, remove=True)
         print("The instance has been removed")
         return
-    else:
-        print("That's not a valid option!")
-        return
 
 
 def main():
@@ -218,6 +346,9 @@ def main():
             remove_instance()
         except NotImplementedError as e:
             print(str(e))
+    elif args.edit:
+        loop = asyncio.get_event_loop()
+        loop.run_until_complete(edit_instance())
     else:
         basic_setup()
 


Test patch: