diff --git a/cvat/apps/engine/backup.py b/cvat/apps/engine/backup.py index 20cd81e0d70..0b5d02a994d 100644 --- a/cvat/apps/engine/backup.py +++ b/cvat/apps/engine/backup.py @@ -8,6 +8,7 @@ import os import re import shutil +import tempfile import uuid from abc import ABCMeta, abstractmethod from collections.abc import Collection, Iterable @@ -46,7 +47,10 @@ retry_current_rq_job, ) from cvat.apps.engine import models -from cvat.apps.engine.cloud_provider import import_resource_from_cloud_storage +from cvat.apps.engine.cloud_provider import ( + db_storage_to_storage_instance, + import_resource_from_cloud_storage, +) from cvat.apps.engine.location import StorageType, get_location_configuration from cvat.apps.engine.log import ServerLogManager from cvat.apps.engine.models import ( @@ -439,8 +443,29 @@ def _write_data(self, zip_object, target_dir=None): files=[self._db_data.get_manifest_path()], target_dir=target_data_dir, ) + elif self._db_data.storage == StorageChoice.CLOUD_STORAGE: + assert not hasattr(self._db_data, 'video'), "Only images can be stored in cloud storage" + media_files = [im.path for im in self._db_data.images.all()] + cloud_storage_instance = db_storage_to_storage_instance(self._db_data.cloud_storage) + with tempfile.TemporaryDirectory() as tmp_dir: + cloud_storage_instance.bulk_download_to_dir(files=media_files, upload_dir=tmp_dir) + self._write_files( + source_dir=tmp_dir, + zip_object=zip_object, + files=[ + os.path.join(tmp_dir, file) + for file in media_files + ], + target_dir=target_data_dir, + ) + self._write_files( + source_dir=self._db_data.get_upload_dirname(), + zip_object=zip_object, + files=[self._db_data.get_manifest_path()], + target_dir=target_data_dir, + ) else: - raise NotImplementedError("We don't currently support backing up tasks with data from cloud storage") + raise NotImplementedError def _write_task(self, zip_object, target_dir=None): task_dir = self._db_task.get_dirname() @@ -539,6 +564,9 @@ def serialize_data(): ] data['validation_layout'] = validation_params + if self._db_data.storage == StorageChoice.CLOUD_STORAGE: + data["storage"] = StorageChoice.LOCAL + return self._prepare_data_meta(data) task = serialize_task() diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index f1ad80f6263..9111c841e80 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -3959,6 +3959,33 @@ def test_cannot_export_backup_for_task_without_data(self, tasks): assert exc.status == HTTPStatus.BAD_REQUEST assert "Backup of a task without data is not allowed" == exc.body.encode() + def test_can_export_and_import_backup_task_with_cloud_storage(self, tasks): + cloud_storage_content = ["image_case_65_1.png", "image_case_65_2.png"] + task_spec = { + "name": "Task with files from cloud storage", + "labels": [ + { + "name": "car", + } + ], + } + data_spec = { + "image_quality": 75, + "use_cache": False, + "cloud_storage_id": 1, + "server_files": cloud_storage_content, + } + task_id, _ = create_task(self.user, task_spec, data_spec) + + task = self.client.tasks.retrieve(task_id) + + filename = self.tmp_dir / f"cloud_task_{task.id}_backup.zip" + task.download_backup(filename) + + assert filename.is_file() + assert filename.stat().st_size > 0 + self._test_can_restore_task_from_backup(task_id) + @pytest.mark.parametrize("mode", ["annotation", "interpolation"]) def test_can_import_backup(self, tasks, mode): task_id = next(t for t in tasks if t["mode"] == mode)["id"]