diff --git a/lib/galaxy/config/sample/datatypes_conf.xml.sample b/lib/galaxy/config/sample/datatypes_conf.xml.sample
index 0e026b5a7b0a..a8e798143f8b 100644
--- a/lib/galaxy/config/sample/datatypes_conf.xml.sample
+++ b/lib/galaxy/config/sample/datatypes_conf.xml.sample
@@ -288,13 +288,14 @@
-
-
-
-
+
+
-
+
+
+
+
diff --git a/lib/galaxy/datatypes/converters/archive_to_directory.xml b/lib/galaxy/datatypes/converters/archive_to_directory.xml
new file mode 100644
index 000000000000..d0c56c154c09
--- /dev/null
+++ b/lib/galaxy/datatypes/converters/archive_to_directory.xml
@@ -0,0 +1,37 @@
+
+
+
+ galaxy-util
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/galaxy/datatypes/converters/tar_to_directory.xml b/lib/galaxy/datatypes/converters/tar_to_directory.xml
index 59354b39b5fd..0160746283da 100644
--- a/lib/galaxy/datatypes/converters/tar_to_directory.xml
+++ b/lib/galaxy/datatypes/converters/tar_to_directory.xml
@@ -1,7 +1,7 @@
- galaxy-util
+ galaxy-util
mkdir '$output1.files_path';
diff --git a/lib/galaxy/datatypes/data.py b/lib/galaxy/datatypes/data.py
index 0024adea6155..e5fea2612ace 100644
--- a/lib/galaxy/datatypes/data.py
+++ b/lib/galaxy/datatypes/data.py
@@ -465,6 +465,7 @@ def _serve_file_download(self, headers, data, trans, to_ext, file_size, **kwd):
composite_extensions = trans.app.datatypes_registry.get_composite_extensions()
composite_extensions.append("html") # for archiving composite datatypes
composite_extensions.append("data_manager_json") # for downloading bundles if bundled.
+ composite_extensions.append("directory") # for downloading directories.
if data.extension in composite_extensions:
return self._archive_composite_dataset(trans, data, headers, do_action=kwd.get("do_action", "zip"))
@@ -1212,6 +1213,18 @@ def regex_line_dataprovider(
class Directory(Data):
"""Class representing a directory of files."""
+ file_ext = "directory"
+
+ def _archive_main_file(
+ self, archive: ZipstreamWrapper, display_name: str, data_filename: str
+ ) -> Tuple[bool, str, str]:
+ """Overwrites the method to not do anything.
+
+ No main file gets added to a directory archive.
+ """
+ error, msg, messagetype = False, "", ""
+ return error, msg, messagetype
+
class GenericAsn1(Text):
"""Class for generic ASN.1 text format"""
diff --git a/lib/galaxy_test/api/test_tools_upload.py b/lib/galaxy_test/api/test_tools_upload.py
index ed32bc92caf0..57ffece984dc 100644
--- a/lib/galaxy_test/api/test_tools_upload.py
+++ b/lib/galaxy_test/api/test_tools_upload.py
@@ -2,12 +2,14 @@
import os
import urllib.parse
from base64 import b64encode
+from typing import cast
import pytest
from tusclient import client
from galaxy.tool_util.verify.test_data import TestDataResolver
from galaxy.util import UNKNOWN
+from galaxy.util.compression_utils import decompress_bytes_to_directory
from galaxy.util.unittest_utils import (
skip_if_github_down,
skip_if_site_down,
@@ -639,6 +641,44 @@ def test_upload_composite_from_bad_tar(self, history_id):
details = self.dataset_populator.get_history_dataset_details(history_id, dataset=dataset, assert_ok=False)
assert details["state"] == "error"
+ def test_upload_zip_directory_roundtrip(self, history_id):
+ testdir = TestDataResolver().get_filename("testdir1.zip")
+ with open(testdir, "rb") as fh:
+ details = self._upload_and_get_details(fh, api="fetch", history_id=history_id, ext="zip", assert_ok=True)
+ assert details["file_ext"] == "zip"
+
+ # Convert/unpack zip to directory.
+ payload = {
+ "src": "hda",
+ "id": details["id"],
+ "source_type": "zip",
+ "target_type": "directory",
+ "history_id": history_id,
+ }
+ create_response = self._post("tools/CONVERTER_archive_to_directory/convert", data=payload)
+ self.dataset_populator.wait_for_job(create_response.json()["jobs"][0]["id"], assert_ok=True)
+ create_response.raise_for_status()
+ directory_dataset = create_response.json()["outputs"][0]
+
+ # Download (compressed) directory and verify structure.
+ content = self.dataset_populator.get_history_dataset_content(
+ history_id, dataset=directory_dataset, to_ext="directory", type="bytes"
+ )
+ dir_path = decompress_bytes_to_directory(cast(bytes, content))
+ expected_directory_structure = {
+ "file1": "File",
+ "file2": "File",
+ "dir1": "Directory",
+ "dir1/file3": "File",
+ }
+ assert dir_path.endswith("testdir1")
+ for path, entry_class in expected_directory_structure.items():
+ path = os.path.join(dir_path, path)
+ if entry_class == "Directory":
+ assert os.path.isdir(path)
+ else:
+ assert os.path.isfile(path)
+
def test_upload_dbkey(self):
with self.dataset_populator.test_history() as history_id:
payload = self.dataset_populator.upload_payload(history_id, "Test123", dbkey="hg19")