Repository: Cog-Creators/Red-DiscordBot

Issue:
The bot is not functional after using `[p]backup` on non-JSON backends.
# Other bugs

#### What were you trying to do?

Do basically anything after doing a backup on PostgreSQL backend - for example run `[p]help`

#### What were you expecting to happen?

Bot to work fine.

#### What actually happened?

Basically nothing works before I restart.

#### How can we reproduce this issue?

Use `[p]backup` on a bot that uses PostgreSQL backend.
Try using `[p]help`



Discussion:
A small update, this also happens on PostgreSQL backend.
I don't really know what exactly happens during migration from non-JSON (i.e. PostgreSQL) to JSON backend (that migration happens before backup) but whatever it does, at the very least it makes all Config instances not aware of their default values (defaults are empty dict) which in result makes the whole bot not functional. After I tried to register global defaults for core conf, I was able to run `[p]help` just fine, but I can't say for sure that missing defaults are the only thing that is broken.

Resolving patch:
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -31,7 +31,6 @@
     i18n,
     config,
 )
-from .utils._internal_utils import create_backup
 from .utils.predicates import MessagePredicate
 from .utils.chat_formatting import (
     box,
@@ -1390,75 +1389,6 @@ async def listlocales(self, ctx: commands.Context):
 
         await ctx.send_interactive(pages, box_lang="Available Locales:")
 
-    @commands.command()
-    @checks.is_owner()
-    async def backup(self, ctx: commands.Context, *, backup_dir: str = None):
-        """Creates a backup of all data for this bot instance.
-
-        This backs up the bot's data and settings.
-        You may provide a path to a directory for the backup archive to
-        be placed in. If the directory does not exist, the bot will
-        attempt to create it.
-        """
-        if backup_dir is None:
-            dest = Path.home()
-        else:
-            dest = Path(backup_dir)
-
-        driver_cls = drivers.get_driver_class()
-        if driver_cls != drivers.JsonDriver:
-            await ctx.send(_("Converting data to JSON for backup..."))
-            async with ctx.typing():
-                await config.migrate(driver_cls, drivers.JsonDriver)
-
-        log.info("Creating backup for this instance...")
-        try:
-            backup_fpath = await create_backup(dest)
-        except OSError as exc:
-            await ctx.send(
-                _(
-                    "Creating the backup archive failed! Please check your console or logs for "
-                    "details."
-                )
-            )
-            log.exception("Failed to create backup archive", exc_info=exc)
-            return
-
-        if backup_fpath is None:
-            await ctx.send(_("Your datapath appears to be empty."))
-            return
-
-        log.info("Backup archive created successfully at '%s'", backup_fpath)
-        await ctx.send(
-            _("A backup has been made of this instance. It is located at `{path}`.").format(
-                path=backup_fpath
-            )
-        )
-        if backup_fpath.stat().st_size > 8_000_000:
-            await ctx.send(_("This backup is too large to send via DM."))
-            return
-        await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
-
-        pred = MessagePredicate.yes_or_no(ctx)
-        try:
-            await ctx.bot.wait_for("message", check=pred, timeout=60)
-        except asyncio.TimeoutError:
-            await ctx.send(_("Response timed out."))
-        else:
-            if pred.result is True:
-                await ctx.send(_("OK, it's on its way!"))
-                try:
-                    async with ctx.author.typing():
-                        await ctx.author.send(
-                            _("Here's a copy of the backup"), file=discord.File(str(backup_fpath))
-                        )
-                except discord.Forbidden:
-                    await ctx.send(_("I don't seem to be able to DM you. Do you have closed DMs?"))
-                except discord.HTTPException:
-                    await ctx.send(_("I could not send the backup file."))
-            else:
-                await ctx.send(_("OK then."))
-
     @commands.command()
     @commands.cooldown(1, 60, commands.BucketType.user)
     async def contact(self, ctx: commands.Context, *, message: str):
diff --git a/redbot/setup.py b/redbot/setup.py
--- a/redbot/setup.py
+++ b/redbot/setup.py
@@ -6,7 +6,7 @@
 import sys
 from copy import deepcopy
 from pathlib import Path
-from typing import Dict, Any, Optional
+from typing import Dict, Any, Optional, Union
 
 import appdirs
 import click
@@ -199,13 +199,13 @@ async def do_migration(
     return new_storage_details
 
 
-async def create_backup(instance: str) -> None:
+async def create_backup(instance: str, destination_folder: Path = Path.home()) -> None:
     data_manager.load_basic_configuration(instance)
     backend_type = get_current_backend(instance)
     if backend_type != BackendType.JSON:
         await do_migration(backend_type, BackendType.JSON)
     print("Backing up the instance's data...")
-    backup_fpath = await red_create_backup()
+    backup_fpath = await red_create_backup(destination_folder)
     if backup_fpath is not None:
         print(f"A backup of {instance} has been made. It is at {backup_fpath}")
     else:
@@ -279,6 +279,7 @@ async def remove_instance_interaction():
 @click.option("--debug", type=bool)
 @click.pass_context
 def cli(ctx, debug):
+    """Create a new instance."""
     level = logging.DEBUG if debug else logging.INFO
     redbot.logging.init_logging(level=level, location=Path.cwd() / "red_setup_logs")
     if ctx.invoked_subcommand is None:
@@ -342,6 +343,7 @@ def delete(
     drop_db: Optional[bool],
     remove_datapath: Optional[bool],
 ):
+    """Removes an instance."""
     loop = asyncio.get_event_loop()
     loop.run_until_complete(
         remove_instance(
@@ -354,6 +356,7 @@ def delete(
 @click.argument("instance", type=click.Choice(instance_list))
 @click.argument("backend", type=click.Choice(["json", "postgres"]))
 def convert(instance, backend):
+    """Convert data backend of an instance."""
     current_backend = get_current_backend(instance)
     target = get_target_backend(backend)
     data_manager.load_basic_configuration(instance)
@@ -379,6 +382,21 @@ def convert(instance, backend):
         )
 
 
+@cli.command()
+@click.argument("instance", type=click.Choice(instance_list))
+@click.argument(
+    "destination_folder",
+    type=click.Path(
+        exists=False, dir_okay=True, file_okay=False, resolve_path=True, writable=True
+    ),
+    default=Path.home(),
+)
+def backup(instance: str, destination_folder: Union[str, Path]) -> None:
+    """Backup instance's data."""
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(create_backup(instance, Path(destination_folder)))
+
+
 if __name__ == "__main__":
     try:
         cli()  # pylint: disable=no-value-for-parameter  # click


Test patch: