Repository: microsoft/torchgeo

Issue:
Eurosat No such file or directory: 'data/eurosat-train.txt'
### Description

With `download=True` this should be downloaded..?

### Steps to reproduce

```
from torchgeo.datasets import EuroSAT

>>> ds = EuroSAT(root="data", download=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/teamspace/studios/this_studio/torchgeo/torchgeo/datasets/eurosat.py", line 144, in __init__
    with open(os.path.join(self.root, self.split_filenames[split])) as f:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'data/eurosat-train.txt'
```

### Version

0.8.0.dev0

Discussion:
@adamjstewart: It's possible you downloaded val/test before downloading train and it only downloaded `eurosat-val.txt`? Either way we should fix this.@robmarkcole: Right run this in a fresh studio and no issue, possibly the error arose since in my earlier studio I had already used `EuroSAT100DataModule`..?@robmarkcole: OK I can get a similar error with Eurosat100 if I first use regular eurosat:
```
FileNotFoundError: [Errno 2] No such file or directory: 'data/eurosat-100-train.txt'
```@adamjstewart: Ah, that would do it. You could use a unique path for your dataset, but we could also fix this to check for the split file download name.@robmarkcole: I would actually expect that with `root="data"` there would be a generated folder structure of something like `data/eurosat100/files`@adamjstewart: That's how torchvision works. That's explicitly not how TorchGeo works, because I didn't want people who use `root='data/eurosat100'` to find that nothing works correctly because they were too specific.

Solution patch:
diff --git a/torchgeo/datasets/eurosat.py b/torchgeo/datasets/eurosat.py
index 5caef0ee..e7454dae 100644
--- a/torchgeo/datasets/eurosat.py
+++ b/torchgeo/datasets/eurosat.py
@@ -15,7 +15,13 @@ from torch import Tensor
 
 from .errors import DatasetNotFoundError, RGBBandsMissingError
 from .geo import NonGeoClassificationDataset
-from .utils import Path, check_integrity, download_url, extract_archive, rasterio_loader
+from .utils import (
+    Path,
+    download_and_extract_archive,
+    download_url,
+    extract_archive,
+    rasterio_loader,
+)
 
 
 class EuroSAT(NonGeoClassificationDataset):
@@ -126,11 +132,12 @@ class EuroSAT(NonGeoClassificationDataset):
            The *bands* parameter.
         """
         self.root = root
+        self.split = split
         self.transforms = transforms
         self.download = download
         self.checksum = checksum
 
-        assert split in ['train', 'val', 'test']
+        assert self.split in {'train', 'val', 'test'}
 
         self._validate_bands(bands)
         self.bands = bands
@@ -173,53 +180,35 @@ class EuroSAT(NonGeoClassificationDataset):
 
         return sample
 
-    def _check_integrity(self) -> bool:
-        """Check integrity of dataset.
-
-        Returns:
-            True if dataset files are found and/or MD5s match, else False
-        """
-        integrity: bool = check_integrity(
-            os.path.join(self.root, self.filename), self.md5 if self.checksum else None
-        )
-        return integrity
-
     def _verify(self) -> None:
         """Verify the integrity of the dataset."""
-        # Check if the files already exist
-        filepath = os.path.join(self.root, self.base_dir)
-        if os.path.exists(filepath):
-            return
+        # Check split file
+        filename = os.path.join(self.root, self.split_filenames[self.split])
+        if not os.path.isfile(filename):
+            if self.download:
+                download_url(
+                    self.url + self.split_filenames[self.split],
+                    self.root,
+                    md5=self.split_md5s[self.split] if self.checksum else None,
+                )
+            else:
+                raise DatasetNotFoundError(self)
 
-        # Check if zip file already exists (if so then extract)
-        if self._check_integrity():
-            self._extract()
+        # Check image directory
+        directory = os.path.join(self.root, self.base_dir)
+        zipfile = os.path.join(self.root, self.filename)
+        if os.path.isdir(directory):
             return
-
-        # Check if the user requested to download the dataset
-        if not self.download:
-            raise DatasetNotFoundError(self)
-
-        # Download and extract the dataset
-        self._download()
-        self._extract()
-
-    def _download(self) -> None:
-        """Download the dataset."""
-        download_url(
-            self.url + self.filename, self.root, md5=self.md5 if self.checksum else None
-        )
-        for split in self.splits:
-            download_url(
-                self.url + self.split_filenames[split],
+        elif os.path.isfile(zipfile):
+            extract_archive(zipfile)
+        elif self.download:
+            download_and_extract_archive(
+                self.url + self.filename,
                 self.root,
-                md5=self.split_md5s[split] if self.checksum else None,
+                md5=self.md5 if self.checksum else None,
             )
-
-    def _extract(self) -> None:
-        """Extract the dataset."""
-        filepath = os.path.join(self.root, self.filename)
-        extract_archive(filepath)
+        else:
+            raise DatasetNotFoundError(self)
 
     def _validate_bands(self, bands: Sequence[str]) -> None:
         """Validate list of bands.


Test patch:
diff --git a/tests/datasets/test_eurosat.py b/tests/datasets/test_eurosat.py
index 282ff581..5ed3dcca 100644
--- a/tests/datasets/test_eurosat.py
+++ b/tests/datasets/test_eurosat.py
@@ -60,19 +60,43 @@ class TestEuroSAT:
         assert len(ds) == 4
 
     def test_already_downloaded(self, dataset: EuroSAT, tmp_path: Path) -> None:
-        type(dataset)(tmp_path)
+        type(dataset)(tmp_path, split=dataset.split)
 
     def test_already_downloaded_not_extracted(
         self, dataset: EuroSAT, tmp_path: Path
     ) -> None:
         shutil.rmtree(dataset.root)
         shutil.copy(dataset.url + dataset.filename, tmp_path)
-        type(dataset)(tmp_path)
+        type(dataset)(tmp_path, split=dataset.split)
 
     def test_not_downloaded(self, tmp_path: Path) -> None:
         with pytest.raises(DatasetNotFoundError, match='Dataset not found'):
             EuroSAT(tmp_path)
 
+    def test_missing_images_and_zip_no_download(self, tmp_path: Path) -> None:
+        """Ensure DatasetNotFoundError is raised if images and zip are missing and download=False."""
+        split_file = tmp_path / 'eurosat-train.txt'
+        split_file.write_text('dummy.tif\n')
+        with pytest.raises(DatasetNotFoundError):
+            EuroSAT(root=tmp_path, split='train', download=False)
+
+    def test_image_folder_present_split_file_missing(
+        self, tmp_path: Path, monkeypatch: MonkeyPatch
+    ) -> None:
+        """Test that split file is downloaded if missing but image folder is present."""
+
+        image_dir = tmp_path / EuroSAT.base_dir
+        class_dir = image_dir / 'AnnualCrop'
+        class_dir.mkdir(parents=True, exist_ok=True)
+        (class_dir / 'AnnualCrop_1.tif').touch()
+        split_file = tmp_path / 'eurosat-train.txt'
+        assert not split_file.exists()
+        monkeypatch.setattr(
+            EuroSAT, 'url', os.path.join('tests', 'data', 'eurosat') + os.sep
+        )
+        EuroSAT(root=tmp_path, split='train', download=True)
+        assert split_file.exists()
+
     def test_plot(self, dataset: EuroSAT) -> None:
         x = dataset[0].copy()
         dataset.plot(x, suptitle='Test')
@@ -84,7 +108,7 @@ class TestEuroSAT:
         plt.close()
 
     def test_plot_rgb(self, dataset: EuroSAT, tmp_path: Path) -> None:
-        dataset = type(dataset)(tmp_path, bands=('B03',))
+        dataset = type(dataset)(tmp_path, split=dataset.split, bands=('B03',))
         with pytest.raises(
             RGBBandsMissingError, match='Dataset does not contain some of the RGB bands'
         ):