This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit bad81f42f15b104bf2c9650601a5d29e18e49ae2
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Sep 20 17:44:22 2024 +0200

    Replace `ResourceOnFileSystem` internal interface by a public 
`Resource.getFileSet()` method.
    Provides default methods for copying and deleting the resource, with 
overrides in GDALStore.
---
 .../org/apache/sis/storage/geotiff/DataCube.java   |   9 +-
 .../sis/storage/geotiff/MultiResolutionImage.java  |  11 +-
 .../sis/storage/netcdf/base/RasterResource.java    |  11 +-
 .../main/org/apache/sis/storage/Resource.java      | 206 ++++++++++++++++++++-
 .../org/apache/sis/storage/base/PRJDataStore.java  |  35 ++--
 .../sis/storage/base/ResourceOnFileSystem.java     |  71 -------
 .../org/apache/sis/storage/base/URIDataStore.java  |  37 ++--
 .../sis/storage/base/URIDataStoreProvider.java     |   9 +-
 .../org/apache/sis/storage/esri/RasterStore.java   |   7 +-
 .../apache/sis/storage/esri/RawRasterStore.java    |   5 +-
 .../main/org/apache/sis/storage/folder/Store.java  |   6 +-
 .../apache/sis/storage/folder/WritableStore.java   |  39 ++--
 .../apache/sis/storage/image/WorldFileStore.java   |   5 +-
 .../sis/storage/esri/AsciiGridStoreTest.java       |  59 +++++-
 .../main/org/apache/sis/storage/gdal/Driver.java   |  99 ++++++++++
 .../main/org/apache/sis/storage/gdal/GDAL.java     |  23 +++
 .../org/apache/sis/storage/gdal/GDALStore.java     |  36 ++--
 .../main/org/apache/sis/storage/gdal/Opener.java   |   9 +-
 .../org/apache/sis/storage/gdal/GDALStoreTest.java |  59 +++++-
 .../apache/sis/storage/geopackage/GpkgStore.java   |  26 +--
 .../apache/sis/storage/gsf/GSFRecordReader.java    |   2 +-
 .../main/org/apache/sis/storage/gsf/GSFStore.java  |   9 +-
 .../sis/storage/shapefile/ShapefileStore.java      |  15 +-
 .../sis/storage/shapefile/ShapefileStoreTest.java  |  31 ++--
 .../org/apache/sis/gui/dataset/PathAction.java     |  27 +--
 25 files changed, 605 insertions(+), 241 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
index a0f32761ab..e6f123f9f7 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
@@ -34,7 +34,6 @@ import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.storage.geotiff.base.Predictor;
 import org.apache.sis.storage.geotiff.base.Compression;
 import org.apache.sis.storage.base.TiledGridResource;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.storage.base.StoreResource;
 import org.apache.sis.math.Vector;
 
@@ -50,7 +49,7 @@ import org.apache.sis.math.Vector;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-abstract class DataCube extends TiledGridResource implements 
ResourceOnFileSystem, StoreResource {
+abstract class DataCube extends TiledGridResource implements StoreResource {
     /**
      * The GeoTIFF reader which contain this {@code DataCube}.
      * Used for fetching information like the input channel and where to 
report warnings.
@@ -111,12 +110,12 @@ abstract class DataCube extends TiledGridResource 
implements ResourceOnFileSyste
     public abstract Optional<GenericName> getIdentifier();
 
     /**
-     * Gets the paths to files used by this resource, or an empty array if 
unknown.
+     * Gets the paths to files used by this resource, or an empty value if 
unknown.
      */
     @Override
-    public final Path[] getComponentFiles() {
+    public final Optional<FileSet> getFileSet() {
         final Path location = reader.store.path;
-        return (location != null) ? new Path[] {location} : new Path[0];
+        return (location != null) ? Optional.of(new FileSet(location)) : 
Optional.empty();
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
index 8e928eb840..7188a27f74 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
@@ -18,8 +18,8 @@ package org.apache.sis.storage.geotiff;
 
 import java.util.List;
 import java.util.Arrays;
+import java.util.Optional;
 import java.io.IOException;
-import java.nio.file.Path;
 import org.opengis.util.NameSpace;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.DirectPosition;
@@ -37,7 +37,6 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.base.StoreResource;
 import org.apache.sis.storage.base.GridResourceWrapper;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.privy.DirectPositionView;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
@@ -49,7 +48,7 @@ import org.apache.sis.referencing.operation.matrix.MatrixSIS;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class MultiResolutionImage extends GridResourceWrapper implements 
ResourceOnFileSystem, StoreResource {
+final class MultiResolutionImage extends GridResourceWrapper implements 
StoreResource {
     /**
      * Name of the image at finest resolution.
      * This is used as the namespace for overviews.
@@ -95,11 +94,11 @@ final class MultiResolutionImage extends 
GridResourceWrapper implements Resource
     }
 
     /**
-     * Gets the paths to files used by this resource, or an empty array if 
unknown.
+     * Gets the paths to files used by this resource, or an empty value if 
unknown.
      */
     @Override
-    public Path[] getComponentFiles() {
-        return levels[0].getComponentFiles();
+    public final Optional<FileSet> getFileSet() {
+        return levels[0].getFileSet();
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java
index ddfb51efb7..af58a891be 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java
@@ -35,7 +35,6 @@ import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.Resource;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.base.StoreResource;
 import org.apache.sis.util.Numbers;
@@ -68,7 +67,7 @@ import org.apache.sis.storage.netcdf.internal.Resources;
  * @author  Johann Sorel (Geomatys)
  * @author  Alexis Manin (Geomatys)
  */
-public final class RasterResource extends AbstractGridCoverageResource 
implements StoreResource, ResourceOnFileSystem {
+public final class RasterResource extends AbstractGridCoverageResource 
implements StoreResource {
     /**
      * Words used in standard (preferred) or long (if no standard) variable 
names which suggest
      * that the variable is a component of a vector. Those words are used in 
heuristic rules
@@ -159,7 +158,7 @@ public final class RasterResource extends 
AbstractGridCoverageResource implement
     /**
      * Path to the netCDF file for information purpose, or {@code null} if 
unknown.
      *
-     * @see #getComponentFiles()
+     * @see #getFileSet()
      */
     private final Path location;
 
@@ -739,11 +738,11 @@ public final class RasterResource extends 
AbstractGridCoverageResource implement
     }
 
     /**
-     * Gets the paths to files used by this resource, or an empty array if 
unknown.
+     * Gets the paths to files used by this resource, or an empty value if 
unknown.
      */
     @Override
-    public Path[] getComponentFiles() {
-        return (location != null) ? new Path[] {location} : new Path[0];
+    public final Optional<FileSet> getFileSet() {
+        return (location != null) ? Optional.of(new FileSet(location)) : 
Optional.empty();
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java
index ab15572895..61022718dc 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java
@@ -16,7 +16,16 @@
  */
 package org.apache.sis.storage;
 
+import java.util.List;
+import java.util.Set;
+import java.util.HashSet;
 import java.util.Optional;
+import java.util.Collection;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.CopyOption;
+import java.nio.file.FileAlreadyExistsException;
 import org.opengis.util.GenericName;
 import org.opengis.metadata.Metadata;
 import org.apache.sis.storage.event.StoreEvent;
@@ -44,7 +53,8 @@ import org.apache.sis.storage.event.StoreListener;
  * The sub-types performing the actual data extraction – for example {@link 
FeatureSet} – are specific to Apache SIS.
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
  *
  * @see Aggregate#components()
  *
@@ -138,6 +148,200 @@ public interface Resource {
      */
     Metadata getMetadata() throws DataStoreException;
 
+    /**
+     * Gets the paths to the files potentially used by this resource.
+     * They are the files that would need to be linked, copied, moved or 
deleted if this resource was
+     * to be transferred efficiently to another location or file system (e.g. 
a <abbr>ZIP</abbr> file).
+     * Such set of files usually exists for {@link DataStore} instances, but 
not for their
+     * {@linkplain DataStore#findResource(String) components} because it is 
often impractical
+     * to copy a single resource component without copying the full data set 
that contains it.
+     *
+     * <p>There are some exceptions to above scenario. A {@link DataStore} may 
have no declared set of files
+     * if the store is generated by an in-memory calculation, or if the store 
uses a connection to a database.
+     * Conversely, a component (child resource) may have a set of files if the 
parent {@code DataStore} stores
+     * each component separately.</p>
+     *
+     * @return files used by this resource.
+     * @throws DataStoreException if an error occurred while preparing the set 
of files.
+     *
+     * @since 1.5
+     */
+    default Optional<FileSet> getFileSet() throws DataStoreException {
+        return Optional.empty();
+    }
+
+    /**
+     * Paths to the files potentially used by the enclosing resource.
+     * They are typically the files given to the {@link DataStore} constructor,
+     * sometime with auxiliary files (metadata, index, <i>etc.</i>).
+     * {@code FileSet} allows efficient copies of data set from one location 
to another.
+     *
+     * <p>Note that some {@code FileSet} items of a given resource may be 
shared by the {@code FileSet}
+     * of another resource. Therefore, there is no guarantee that modifying or 
deleting a file will not
+     * impact other resources.</p>
+     *
+     * @author  Johann Sorel (Geomatys)
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.5
+     * @since   1.5
+     */
+    public static class FileSet {
+        /**
+         * The files to be returned by {@link #getPaths()}.
+         */
+        private final Collection<Path> paths;
+
+        /**
+         * Creates a new instance with the given path.
+         * This is a convenience constructor for the common case where the 
data store uses exactly one file.
+         *
+         * @param  paths  the single file to be returned by {@link 
#getPaths()}.
+         */
+        public FileSet(final Path path) {
+            this.paths = List.of(path);
+        }
+
+        /**
+         * Creates a new instance with the given paths.
+         *
+         * @param  paths  the files to be returned by {@link #getPaths()}.
+         */
+        public FileSet(final Path[] paths) {
+            this.paths = List.of(paths);
+        }
+
+        /**
+         * Creates a new instance with the given paths.
+         * The content of the given collection is copied.
+         *
+         * @param  paths  the files to be returned by {@link #getPaths()}.
+         */
+        public FileSet(final Collection<Path> paths) {
+            this.paths = List.copyOf(paths);
+        }
+
+        /**
+         * Returns the paths to the files potentially used by the enclosing 
resource.
+         * The first file in the returned collection should be the main file 
given to the {@link DataStore}
+         * constructor or opened by other kinds of resource. All other files, 
if any, may be auxiliary files
+         * such as metadata or index. The files are not necessarily in the 
same directory,
+         * and the same files may be used by more than one resource.
+         *
+         * <p>The returned paths should be regular files instead of 
directories, unless the enclosing
+         * resource is designed for using the content of the whole directory. 
The collection should not
+         * contain non-existent files.</p>
+         *
+         * @return paths to the files potentially used by the enclosing 
resource.
+         */
+        @SuppressWarnings("ReturnOfCollectionOrArrayField")
+        public Collection<Path> getPaths() {
+            return paths;
+        }
+
+        /**
+         * Returns the directory which contains all files, or {@code null} if 
none.
+         * This is either the directory of the main file, or a parent 
directory if
+         * an auxiliary file is found there. The latter case happens when the 
same
+         * file (e.g. a global {@code metadata.xml}) is shared by many 
resources.
+         */
+        private Path getBaseDirectory() {
+            Path directory = null;
+            for (Path path : getPaths()) {
+                path = path.getParent();
+                if (directory == null || directory.startsWith(path)) {
+                    directory = path;
+                }
+            }
+            return directory;
+        }
+
+        /**
+         * Copies all resource files to the given directory. The copied files 
are usually the files returned
+         * by {@link #getPaths()}, but details may very depending on the 
{@link DataStore} implementation.
+         * It is recommended to invoke this method only when the {@link 
DataStore} is closed.
+         *
+         * <p><b>Limitations:</b></p>
+         * <ul>
+         *   <li>This method does not overwrite existing files or 
sub-directories.
+         *       If a destination already exists, an exception should be 
thrown.</li>
+         *   <li>This copy operation is not atomic. If an exception is thrown,
+         *       some files may be only partially copied or not created at 
all.</li>
+         * </ul>
+         *
+         * This method can be used for exporting the resource to a 
<abbr>ZIP</abbr> file if the given
+         * destination directory is associated to the file system of the 
{@code jdk.zipfs} module.
+         *
+         * <h4>Default implementation</h4>
+         * The default implementation performs a {@linkplain Files#copy(Path, 
Path, CopyOption...) copy operation}
+         * for each item returned by {@link #getPath()}. If those source files 
are in different directories,
+         * the default implementation reproduces the directory structure which 
is below the common parent of
+         * all source files.
+         *
+         * @param  destDir  the directory where to copy the resource files.
+         * @return the copied main file. May be in a sub-directory of {@code 
destDir}.
+         * @throws FileAlreadyExistsException if a destination file or 
directory already exists.
+         * @throws IOException if another error occurred while copying the 
files.
+         *
+         * @see Files#copy(Path, Path, CopyOption...)
+         */
+        public Path copy(final Path destDir) throws IOException {
+            final var subdirs = new HashSet<Path>();
+            final Path base = getBaseDirectory();
+            Path main = null;
+            for (Path source : getPaths()) {
+                Path target = source;
+                if (base != null) {
+                    target = base.relativize(target);
+                    mkdirs(destDir, target, subdirs);
+                }
+                target = Files.copy(source, destDir.resolve(target));
+                if (main == null) main = target;
+            }
+            return main;
+        }
+
+        /**
+         * Creates the parent directories if they are needed.
+         * We do not use {@code Files.mkdirs(…)} because we want this method 
to fail if a directory exists.
+         */
+        private static void mkdirs(final Path destDir, Path file, final 
Set<Path> done) throws IOException {
+            file = file.getParent();
+            if (file != null && done.add(file)) {
+                mkdirs(destDir, file, done);   // Create parents first.
+                Files.createDirectory(destDir.resolve(file));
+            }
+        }
+
+        /**
+         * Deletes the files used by the enclosing resource.
+         * The {@link DataStore} that contains the resource should be closed 
before to invoke this method.
+         * This is not an atomic operation. If an exception is thrown, some 
files may still remain.
+         *
+         * <h4>Default implementation</h4>
+         * The default implementation {@linkplain Files#delete(Path) deletes} 
the files returned by
+         * {@link #getPaths()} that are in the same directory or a 
sub-directory of the main file.
+         * Files in parent directory are not deleted, because they are often 
files shared by many
+         * resources (e.g. a global {@code metadata.xml} file).
+         *
+         * @throws IOException if another error occurred while deleting a file.
+         *
+         * @see Files#delete(Path)
+         */
+        public void delete() throws IOException {
+            Path base = null;
+            boolean first = true;
+            for (Path path : getPaths()) {
+                if (first) {
+                    first = false;
+                    base = path.getParent();    // May still null.
+                } else if (base != null && !path.startsWith(base)) {
+                    continue;
+                }
+                Files.delete(path);
+            }
+        }
+    }
+
     /**
      * Registers a listener to notify when the specified kind of event occurs 
in this resource or in children.
      * The resource will call the {@link 
StoreListener#eventOccured(StoreEvent)} method when new events matching
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
index 99e5a9864f..dcecea92b8 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
@@ -25,9 +25,9 @@ import java.nio.file.Path;
 import java.nio.file.NoSuchFileException;
 import java.text.ParseException;
 import java.text.ParsePosition;
-import java.util.Arrays;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.ArrayList;
 import jakarta.xml.bind.JAXBException;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
@@ -44,7 +44,6 @@ import org.apache.sis.storage.wkt.StoreFormat;
 import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
-import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.xml.privy.ExceptionSimplifier;
@@ -67,7 +66,7 @@ public abstract class PRJDataStore extends URIDataStore {
     /**
      * The filename extension of {@code "*.prj"} files.
      *
-     * @see #getComponentFiles()
+     * @see #getFileSet()
      */
     protected static final String PRJ = "prj";
 
@@ -218,46 +217,42 @@ public abstract class PRJDataStore extends URIDataStore {
      * The default implementation does the same computation as the 
super-class, then adds the sibling
      * file with {@code ".prj"} extension if it exists.
      *
-     * @return the main file and auxiliary files as paths, or an empty array 
if unknown.
+     * @return the main file and auxiliary files as paths, or an empty value 
if unknown.
      * @throws DataStoreException if the URI cannot be converted to a {@link 
Path}.
      */
     @Override
-    public Path[] getComponentFiles() throws DataStoreException {
+    public Optional<FileSet> getFileSet() throws DataStoreException {
         return listComponentFiles(PRJ);
     }
 
     /**
      * Returns the {@linkplain #location} as a {@code Path} component together 
with auxiliary files.
-     * This method computes the path to the main file as {@link 
URIDataStore#getComponentFiles()},
-     * then add the sibling files with all extensions specified in the {@code 
auxiliaries} argument.
+     * This method computes the path to the main file as {@link 
URIDataStore#getFileSet()},
+     * then adds the sibling files with all extensions specified in the {@code 
auxiliaries} argument.
      * Each auxiliary file is tested for existence. Paths that are not regular 
files are omitted.
-     * This is a helper method for {@link #getComponentFiles()} implementation.
+     * This is a helper method for {@link #getFileSet()} implementations.
      *
      * @param  auxiliaries  filename extension (without leading dot) of all 
auxiliary files.
      *         Null elements are silently ignored.
      * @return the URI as a path, followed by all auxiliary files that exist.
      * @throws DataStoreException if the URI cannot be converted to a {@link 
Path}.
      */
-    protected final Path[] listComponentFiles(final String... auxiliaries) 
throws DataStoreException {
-        Path[] paths = super.getComponentFiles();
-        int count = paths.length;
-        if (count != 0) {
-            final Path path = paths[0];
+    protected final Optional<FileSet> listComponentFiles(final String... 
auxiliaries) throws DataStoreException {
+        return super.getFileSet().map((fileset) -> {
+            final var paths = new ArrayList<Path>(fileset.getPaths());
+            final Path path = paths.get(0);  // This list is ever empty.
             final String base = getBaseFilename(path);
+            boolean modified = false;
             for (final String extension : auxiliaries) {
                 if (extension != null) {
                     final Path p = path.resolveSibling(base.concat(extension));
                     if (Files.isRegularFile(p)) {
-                        if (count >= paths.length) {
-                            paths = Arrays.copyOf(paths, count + 
auxiliaries.length);
-                        }
-                        paths[count++] = p;
+                        modified |= paths.add(p);
                     }
                 }
             }
-            paths = ArraysExt.resize(paths, count);
-        }
-        return paths;
+            return modified ? new FileSet(paths) : fileset;
+        });
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/ResourceOnFileSystem.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/ResourceOnFileSystem.java
deleted file mode 100644
index f24c155bff..0000000000
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/ResourceOnFileSystem.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.storage.base;
-
-import java.nio.file.Path;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.Resource;
-
-
-/**
- * A resource which is loaded from one or many files on an arbitrary file 
system. This interface
- * allows a resource (typically a {@linkplain org.apache.sis.storage.DataStore 
data store}) to
- * list the files that it uses. It may be used for copying or deleting 
resources if the caller
- * is certain that those files are not in use.
- *
- * <h2>Alternatives</h2>
- * <p>For copying data from one location to another, consider using
- * {@link org.apache.sis.storage.WritableAggregate#add(Resource)} instead.
- * The data store implementations may detect that some {@code add(…)} 
operations
- * can be performed by verbatim copy of files.</p>
- *
- * <p>For deleting data, consider using
- * {@link org.apache.sis.storage.WritableAggregate#remove(Resource)} 
instead.</p>
- *
- * @author  Johann Sorel (Geomatys)
- */
-public interface ResourceOnFileSystem extends Resource {
-    /**
-     * Gets the paths to files potentially used by this resource.
-     * This is typically the files opened by a {@linkplain 
org.apache.sis.storage.DataStore data store}.
-     * There is no guarantee that all files are in the same directory or that 
each file is used exclusively
-     * by this data source (e.g. no guarantee that modifying or deleting a 
file will not impact other resources).
-     *
-     * <p>This method should return paths to files only.
-     * It should not return paths to directories.
-     * The caller should verify that all paths are regular files;
-     * non-existent paths should be omitted.</p>
-     *
-     * <h4>Example</h4>
-     * A resources created for a GRIB file may use the following component 
files:
-     * <ul>
-     *   <li>The main GRIB file.</li>
-     *   <li>If managed by the UCAR library, two auxiliary files next to the 
main GRIB file:
-     *       the index file ({@code ".gbx9"}) and the collection file ({@code 
".ncx"}).
-     *       Those two files are owned exclusively by the resource.</li>
-     *   <li>Eventually a GRIB table file. This table may be located in a path 
unrelated to
-     *       to the path of the main file and may be shared by many 
resources.</li>
-     * </ul>
-     *
-     * @return files used by this resource. Should never be {@code null}.
-     * @throws DataStoreException if an error on the file system prevent the 
creation of the list.
-     *
-     * @todo Should be renamed to something else, because current name creates 
a confusion with
-     *       {@link org.apache.sis.storage.Aggregate#components()}.
-     */
-    Path[] getComponentFiles() throws DataStoreException;
-}
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
index 2eb90a5382..0be020891b 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
@@ -67,7 +67,7 @@ import org.apache.sis.xml.privy.ExceptionSimplifier;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-public abstract class URIDataStore extends DataStore implements StoreResource, 
ResourceOnFileSystem {
+public abstract class URIDataStore extends DataStore implements StoreResource {
     /**
      * The {@link DataStoreProvider#LOCATION} parameter value, or {@code null} 
if none.
      */
@@ -76,7 +76,7 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource, R
     /**
      * The {@link #location} as a path, or {@code null} if none or if the URI 
cannot be converted to a path.
      *
-     * @see #getComponentFiles()
+     * @see #getFileSet()
      */
     protected final Path locationAsPath;
 
@@ -177,22 +177,28 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource, R
     }
 
     /**
-     * Returns the main and metadata locations as {@code Path} components, or 
an empty array if none.
+     * Returns the main and metadata locations as {@code Path} components, or 
an empty value if none.
      * The default implementation returns the storage specified at 
construction time converted to a
-     * {@link Path} if such conversion was possible, or an empty array 
otherwise. The array may also
+     * {@link Path} if such conversion was possible, or an empty value 
otherwise. The set may also
      * contains the path to the {@linkplain DataOptionKey#METADATA_PATH 
auxiliary metadata file}.
      *
-     * @return the URI to component files as paths, or an empty array if 
unknown.
+     * @return the URI to component files as paths, or an empty value if 
unknown.
      * @throws DataStoreException if an error occurred while getting the paths.
      */
     @Override
-    public Path[] getComponentFiles() throws DataStoreException {
+    public Optional<FileSet> getFileSet() throws DataStoreException {
+        final Path[] paths;
         try {
-            final var paths = new Path[] {locationAsPath, getMetadataPath()};
-            return ArraysExt.resize(paths, ArraysExt.removeDuplicated(paths, 
ArraysExt.removeNulls(paths)));
+            paths = new Path[] {locationAsPath, getMetadataPath()};
         } catch (IOException e) {
             throw new DataStoreException(e);
         }
+        final int count = ArraysExt.removeDuplicated(paths, 
ArraysExt.removeNulls(paths));
+        if (count != 0) {
+            return Optional.of(new FileSet(ArraysExt.resize(paths, count)));
+        } else {
+            return Optional.empty();
+        }
     }
 
     /**
@@ -510,12 +516,15 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource, R
      */
     protected final void deleteAuxiliaryFile(final String extension) throws 
DataStoreException, IOException {
         String previous = null;
-        for (Path path : getComponentFiles()) {
-            final String base = getBaseFilename(path);
-            if (!base.equals(previous)) {
-                previous = base;
-                path = path.resolveSibling(base.concat(extension));
-                Files.deleteIfExists(path);
+        final Optional<FileSet> files = getFileSet();
+        if (files.isPresent()) {
+            for (Path path : files.get().getPaths()) {
+                final String base = getBaseFilename(path);
+                if (!base.equals(previous)) {
+                    previous = base;
+                    path = path.resolveSibling(base.concat(extension));
+                    Files.deleteIfExists(path);
+                }
             }
         }
     }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
index f70df8e68e..911ab593fc 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
@@ -164,14 +164,9 @@ public abstract class URIDataStoreProvider extends 
DataStoreProvider {
         /*
          * This fallback should not happen with `URIDataStore` implementation 
because the "location" parameter
          * is always present even if null. This fallback is for resources 
implementated by different classes.
+         * The first path is presumed the main file.
          */
-        if (resource instanceof ResourceOnFileSystem) {
-            final Path[] paths = ((ResourceOnFileSystem) 
resource).getComponentFiles();
-            if (paths != null && paths.length != 0) {
-                return paths[0];                                    // First 
path is presumed the main file.
-            }
-        }
-        return null;
+        return resource.getFileSet().flatMap((files) -> 
files.getPaths().stream().findFirst()).orElse(null);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
index 1d35e79ad3..3b5f993518 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.Hashtable;
 import java.util.Locale;
+import java.util.Optional;
 import java.io.IOException;
 import java.io.FileNotFoundException;
 import java.nio.file.NoSuchFileException;
@@ -86,7 +87,7 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
     /**
      * The filename extension of {@code "*.stx"} and {@code "*.clr"} files.
      *
-     * @see #getComponentFiles()
+     * @see #getFileSet()
      */
     static final String STX = "stx", CLR = "clr";
 
@@ -132,11 +133,11 @@ abstract class RasterStore extends PRJDataStore 
implements GridCoverageResource
     /**
      * Returns the {@linkplain #location} as a {@code Path} component together 
with auxiliary files.
      *
-     * @return the main file and auxiliary files as paths, or an empty array 
if unknown.
+     * @return the main file and auxiliary files as paths, or an empty value 
if unknown.
      * @throws DataStoreException if the URI cannot be converted to a {@link 
Path}.
      */
     @Override
-    public Path[] getComponentFiles() throws DataStoreException {
+    public Optional<FileSet> getFileSet() throws DataStoreException {
         return listComponentFiles(PRJ, STX, CLR);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
index 0da1ed09f6..99d2d8e0d3 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
@@ -18,6 +18,7 @@ package org.apache.sis.storage.esri;
 
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 import java.io.IOException;
 import java.nio.ByteOrder;
 import java.nio.file.Path;
@@ -193,11 +194,11 @@ final class RawRasterStore extends RasterStore {
     /**
      * Returns the {@linkplain #location} as a {@code Path} component together 
with auxiliary files.
      *
-     * @return the main file and auxiliary files as paths, or an empty array 
if unknown.
+     * @return the main file and auxiliary files as paths, or an empty value 
if unknown.
      * @throws DataStoreException if the URI cannot be converted to a {@link 
Path}.
      */
     @Override
-    public Path[] getComponentFiles() throws DataStoreException {
+    public Optional<FileSet> getFileSet() throws DataStoreException {
         return listComponentFiles(RawRasterStoreProvider.HDR, PRJ, STX, CLR);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
index f59400c987..62fbf4df1c 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
@@ -258,7 +258,7 @@ class Store extends DataStore implements StoreResource, 
UnstructuredAggregate, D
     @Override
     public synchronized Metadata getMetadata() {
         if (metadata == null) {
-            final MetadataBuilder mb = new MetadataBuilder();
+            final var mb = new MetadataBuilder();
             mb.addResourceScope(ScopeCode.COLLECTION, 
Resources.formatInternational(Resources.Keys.DirectoryContent_1, 
getDisplayName()));
             mb.addLanguage(configuration.getOption(OptionKey.LOCALE),
                            configuration.getOption(OptionKey.ENCODING),
@@ -292,8 +292,8 @@ class Store extends DataStore implements StoreResource, 
UnstructuredAggregate, D
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public synchronized Collection<Resource> components() throws 
DataStoreException {
         if (components == null) {
-            final List<DataStore> resources = new ArrayList<>();
-            final NameFactory nameFactory = DefaultNameFactory.provider();
+            final var resources = new ArrayList<DataStore>();
+            final var nameFactory = DefaultNameFactory.provider();
             try (DirectoryStream<Path> stream = 
Files.newDirectoryStream(location, this)) {
                 for (final Path candidate : stream) {
                     /*
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/WritableStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/WritableStore.java
index f553e6549a..bc55ac81cc 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/WritableStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/WritableStore.java
@@ -36,7 +36,6 @@ import org.apache.sis.storage.WritableAggregate;
 import org.apache.sis.storage.WritableFeatureSet;
 import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.base.StoreUtilities;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.io.stream.IOUtilities;
 
@@ -142,7 +141,7 @@ final class WritableStore extends Store implements 
WritableAggregate {
 
     /**
      * Removes a {@code Resource} from this store. The resource must be a part 
of this {@code Aggregate}.
-     * For a folder store, this means that the resource must be a direct 
children of the directory managed
+     * For a folder store, this means that the resource must be a direct child 
of the directory managed
      * by this store.
      *
      * This operation is destructive: the {@link Resource} and it's related 
files will be deleted.
@@ -158,25 +157,25 @@ final class WritableStore extends Store implements 
WritableAggregate {
                     children.remove(path);
                     return;
                 }
-            } else if (resource instanceof ResourceOnFileSystem) {
-                final Path[] componentPaths = ((ResourceOnFileSystem) 
resource).getComponentFiles().clone();
-                for (Path root : componentPaths) {
-                    root = root.getParent();
-                    if (Files.isSameFile(root, location)) {
-                        /*
-                         * If we enter in this block, we have determined that 
at least one file is located in the
-                         * directory managed by this store - NOT in a 
subdirectory since they could be managed by
-                         * different folder stores. We assume that this root 
file is the "main" file. Other files
-                         * could be in subdirectories, but we need to verify - 
we do not delete files outside.
-                         */
-                        for (final Path path : componentPaths) {
-                            if (path.startsWith(root)) {
-                                Files.delete(path);
-                            }
+            } else {
+                final FileSet fileset = resource.getFileSet().orElse(null);
+                if (fileset != null) {
+                    for (Path root : fileset.getPaths()) {
+                        root = root.getParent();
+                        if (root != null && Files.isSameFile(root, location)) {
+                            /*
+                             * If we enter in this block, we have determined 
that the main file is located in the
+                             * directory managed by this store - NOT in a 
subdirectory because they could be managed
+                             * by different folder stores. Note that the 
default implementation of `FileSet.delete()`
+                             * will not delete any file in the parent 
directory. This is the desired behavior because
+                             * such file could be shared by many resources 
(e.g. a global `metadata.xml` file).
+                             */
+                            fileset.delete();
+                            children.values().removeIf((e) -> e == resource);
+                            components = null;      // Clear cache. TODO: we 
should do something more efficient.
+                            return;
                         }
-                        children.values().removeIf((e) -> e == resource);
-                        components = null;      // Clear cache. TODO: we 
should do something more efficient.
-                        return;
+                        break;          // Check only the first file, which 
should be the main file.
                     }
                 }
             }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
index 6b041d655e..36b095adf6 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
@@ -20,6 +20,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Map;
 import java.util.HashMap;
+import java.util.Optional;
 import java.util.logging.Level;
 import java.io.Closeable;
 import java.io.IOException;
@@ -445,11 +446,11 @@ loop:   for (int convention=0;; convention++) {
     /**
      * Returns paths to the main file together with auxiliary files.
      *
-     * @return paths to the main file and auxiliary files, or an empty array 
if unknown.
+     * @return paths to the main file and auxiliary files, or an empty value 
if unknown.
      * @throws DataStoreException if the URI cannot be converted to a {@link 
Path}.
      */
     @Override
-    public synchronized Path[] getComponentFiles() throws DataStoreException {
+    public Optional<FileSet> getFileSet() throws DataStoreException {
         if (suffixWLD == null) try {
             getGridGeometry(MAIN_IMAGE);                // Will compute 
`suffixWLD` as a side effect.
         } catch (URISyntaxException | IOException e) {
diff --git 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/esri/AsciiGridStoreTest.java
 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/esri/AsciiGridStoreTest.java
index 67a74ab967..e4c06e0d50 100644
--- 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/esri/AsciiGridStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/esri/AsciiGridStoreTest.java
@@ -19,6 +19,9 @@ package org.apache.sis.storage.esri;
 import java.util.List;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.apache.sis.coverage.Category;
@@ -31,6 +34,7 @@ import org.apache.sis.storage.ProbeResult;
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
+import static org.apache.sis.test.Assertions.assertMultilinesEquals;
 import static org.apache.sis.test.TestUtilities.getSingleton;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -43,6 +47,11 @@ import org.opengis.metadata.identification.Identification;
  * @author  Martin Desruisseaux (Geomatys)
  */
 public final class AsciiGridStoreTest extends TestCase {
+    /**
+     * Filename of the test file.
+     */
+    private static final String FILENAME = "grid.asc";
+
     /**
      * Creates a new test case.
      */
@@ -53,7 +62,7 @@ public final class AsciiGridStoreTest extends TestCase {
      * Returns a storage connector with the URL to the test data.
      */
     private static StorageConnector testData() {
-        return new 
StorageConnector(AsciiGridStoreTest.class.getResource("grid.asc"));
+        return new 
StorageConnector(AsciiGridStoreTest.class.getResource(FILENAME));
     }
 
     /**
@@ -70,7 +79,7 @@ public final class AsciiGridStoreTest extends TestCase {
     }
 
     /**
-     * Tests the metadata of the {@code "grid.asc"} file. This test reads only 
the header.
+     * Tests the metadata of the {@value #FILENAME} file. This test reads only 
the header.
      * It should not test sample dimensions or pixel values, because doing so 
read the full
      * image and is the purpose of {@link #testRead()}.
      *
@@ -105,7 +114,7 @@ public final class AsciiGridStoreTest extends TestCase {
     }
 
     /**
-     * Tests reading a few values from the {@code "grid.asc"} file.
+     * Tests reading a few values from the {@value #FILENAME} file.
      *
      * @throws DataStoreException if an error occurred while reading the file.
      */
@@ -136,4 +145,48 @@ public final class AsciiGridStoreTest extends TestCase {
             assertSame(coverage, store.read(null, null));
         }
     }
+
+    /**
+     * Tests {@link AsciiGridStore#getFileSet()}. Since {@link AsciiGridStore} 
inherits
+     * the default implementation, this is actually a test of {@code 
Resource.FileSet}.
+     *
+     * @throws DataStoreException if an error occurred while fetching the list 
of files.
+     * @throws IOException if an error occurred while copying the file.
+     */
+    @Test
+    public void testFileSet() throws DataStoreException, IOException {
+        AsciiGridStore.FileSet fileset;
+        try (AsciiGridStore store = new AsciiGridStore(null, testData(), 
true)) {
+            fileset = store.getFileSet().orElseThrow();
+        }
+        final Path source = fileset.getPaths().iterator().next();
+        assertEquals(FILENAME, source.getFileName().toString());
+        /*
+         * Tests the copy operation in a temporary directory.
+         * This is using the default implementation of `FileSet.copy(…)`,
+         */
+        final Path dir = Files.createTempDirectory("sis-");
+        Path target = null;
+        try {
+            target = fileset.copy(dir);
+            assertEquals(dir, target.getParent());
+            assertEquals(FILENAME, target.getFileName().toString());
+            assertMultilinesEquals(Files.readString(source), 
Files.readString(target));
+            /*
+             * In order to test the delete operation, we need to open a new 
data store on the file
+             * that we just copied. Otherwise, `fileset.delete()` would delete 
the original file.
+             */
+            try (AsciiGridStore store = new AsciiGridStore(null,  new 
StorageConnector(target), true)) {
+                fileset = store.getFileSet().orElseThrow();
+            }
+            fileset.delete();
+            assertTrue(Files.notExists(target));
+            target = null;
+        } finally {
+            if (target != null) {
+                Files.deleteIfExists(target);
+            }
+            Files.deleteIfExists(dir);
+        }
+    }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Driver.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Driver.java
index 3b0842a556..84cbda2c3b 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Driver.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Driver.java
@@ -19,6 +19,9 @@ package org.apache.sis.storage.gdal;
 import java.util.List;
 import java.util.AbstractList;
 import java.util.Objects;
+import java.net.URI;
+import java.nio.file.Path;
+import java.io.IOException;
 import java.lang.foreign.Arena;
 import java.lang.foreign.FunctionDescriptor;
 import java.lang.foreign.Linker;
@@ -32,6 +35,7 @@ import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.privy.Strings;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.DataStoreException;
 
 
@@ -165,6 +169,101 @@ public final class Driver {
         return GDAL.toString(result);
     }
 
+    /**
+     * List of files of a <abbr>GDAL</abbr> data set, together with methods 
for copying or deleting them.
+     */
+    final class FileList extends Resource.FileSet {
+        /** Location of the data store as a path. */
+        private final Path path;
+
+        /** Location of the data store as a URL. */
+        private final URI location;
+
+        /** Creates a new file set for the given files. */
+        FileList(final Path[] paths, final Path path, final URI location) {
+            super(paths);
+            this.path = path;
+            this.location = location;
+        }
+
+        /**
+         * Returns the URL (<abbr>GDAL</abbr> syntax) to the data set.
+         */
+        private String getURL() {
+            return Opener.toURL(location, path, true);
+        }
+
+        /**
+         * Copies the files to the given directory. The source should be an 
<abbr>URL</abbr> recognized
+         * by <abbr>GDAL</abbr> because {@code FileList} can be returned only 
by {@link GDALStore}.
+         * However, the destination could be a pure-Java file system.
+         */
+        @Override
+        public Path copy(final Path destDir) throws IOException {
+            final Path   dest   = destDir.resolve(path.getFileName());
+            final String target = Opener.toURL(dest.toUri(), dest, false);
+            if (target != null) try {
+                copyDataSet(getURL(), target);
+                return dest;
+            } catch (DataStoreException e) {
+                throw new IOException(e);
+            } else {
+                return super.copy(destDir);
+            }
+        }
+
+        /**
+         * Deletes the files of the data set.
+         */
+        @Override
+        public void delete() throws IOException {
+            try {
+                deleteDataSet(getURL());
+            } catch (DataStoreException e) {
+                throw new IOException(e);
+            }
+        }
+    }
+
+    /**
+     * Copies the files of a data set managed by this driver.
+     * It is caller responsibility to ensure that this driver is the correct 
one for the data set to copy.
+     * No {@link GDALStore} should be opened on the source at the time that 
this method is invoked.
+     *
+     * @param  source  path or <abbr>URL</abbr> (<abbr>GDAL</abbr> syntax) of 
the main file of the data set to copy.
+     * @param  target  path or <abbr>URL</abbr> (<abbr>GDAL</abbr> syntax) of 
the desired target file.
+     * @throws DataStoreException if an error occurred while invoking a 
<abbr>GDAL</abbr> function.
+     */
+    public void copyDataSet(final String source, final String target) throws 
DataStoreException {
+        final int err;
+        final var gdal = owner.GDAL().copyDataset;
+        try (var arena = Arena.ofConfined()) {
+            err = (int) gdal.invokeExact(handle, arena.allocateFrom(target), 
arena.allocateFrom(source));
+        } catch (Throwable e) {
+            throw GDAL.propagate(e);
+        }
+        ErrorHandler.checkCPLErr(err);
+    }
+
+    /**
+     * Deletes the files of a data set managed by this driver.
+     * It is caller responsibility to ensure that this driver is the correct 
one for the data set to delete.
+     * No {@link GDALStore} should be opened on the data set when this method 
is invoked.
+     *
+     * @param  dataset  path or <abbr>URL</abbr> (<abbr>GDAL</abbr> syntax) of 
the main file of the data set to delete.
+     * @throws DataStoreException if an error occurred while invoking a 
<abbr>GDAL</abbr> function.
+     */
+    public void deleteDataSet(final String dataset) throws DataStoreException {
+        final int err;
+        final var gdal = owner.GDAL().deleteDataset;
+        try (var arena = Arena.ofConfined()) {
+            err = (int) gdal.invokeExact(handle, arena.allocateFrom(dataset));
+        } catch (Throwable e) {
+            throw GDAL.propagate(e);
+        }
+        ErrorHandler.checkCPLErr(err);
+    }
+
     /**
      * Returns the <abbr>GDAL</abbr> version number together with the list of 
drivers.
      * See {@link GDALStoreProvider#configuration()} for the public 
documentation.
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java
index 246aeb96b9..a8a6d2ee6e 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDAL.java
@@ -93,6 +93,18 @@ final class GDAL extends NativeFunctions {
      */
     final MethodHandle identifyDriver;
 
+    /**
+     * <abbr>GDAL</abbr> {@code CPLErr GDALCopyDatasetFiles(GDALDriverH, const 
char *pszNewName, const char *pszOldName)}.
+     * Copy the files of a dataset.
+     */
+    final MethodHandle copyDataset;
+
+    /**
+     * <abbr>GDAL</abbr> {@code CPLErr GDALDeleteDataset(GDALDriverH, const 
char*)}.
+     * Delete named dataset.
+     */
+    final MethodHandle deleteDataset;
+
     /**
      * <abbr>GDAL</abbr> {@code GDALDatasetH GDALOpenEx(const char 
*pszFilename, …)}.
      * Opens a raster or vector file by invoking the open method of each 
driver in turn.
@@ -304,6 +316,17 @@ final class GDAL extends NativeFunctions {
                 ValueLayout.ADDRESS,    // const char* name
                 ValueLayout.ADDRESS));  // const char* domain
 
+        copyDataset = lookup(linker, "GDALCopyDatasetFiles", 
FunctionDescriptor.of(
+                ValueLayout.JAVA_INT,   // CPLErr (return type)
+                ValueLayout.ADDRESS,    // GDALDriverH
+                ValueLayout.ADDRESS,    // const char* pszNewName
+                ValueLayout.ADDRESS));  // const char* pszOldName
+
+        deleteDataset = lookup(linker, "GDALDeleteDataset", 
FunctionDescriptor.of(
+                ValueLayout.JAVA_INT,   // CPLErr (return type)
+                ValueLayout.ADDRESS,    // GDALDriverH
+                ValueLayout.ADDRESS));  // const char* pszName
+
         // For Opener
         close = lookup(linker, "GDALClose",  acceptPointerReturnInt);
         open  = lookup(linker, "GDALOpenEx", 
FunctionDescriptor.of(ValueLayout.ADDRESS,
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java
index f5272fc1a7..57d60b202c 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/GDALStore.java
@@ -44,7 +44,6 @@ import org.apache.sis.storage.DataStoreClosedException;
 import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.base.MetadataBuilder;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.storage.base.URIDataStore;
 import org.apache.sis.util.privy.UnmodifiableArrayList;
 import org.apache.sis.util.iso.DefaultNameFactory;
@@ -60,7 +59,7 @@ import org.apache.sis.system.Cleaners;
  * @version 1.5
  * @since   1.5
  */
-public class GDALStore extends DataStore implements Aggregate, 
ResourceOnFileSystem {
+public class GDALStore extends DataStore implements Aggregate {
     /**
      * The {@link GDALStoreProvider#LOCATION} parameter value, or {@code null} 
if none.
      *
@@ -71,7 +70,7 @@ public class GDALStore extends DataStore implements 
Aggregate, ResourceOnFileSys
     /**
      * Path of the file opened by this data store, or {@code null} if none.
      *
-     * @see #getComponentFiles()
+     * @see #getFileSet()
      */
     private final Path path;
 
@@ -154,7 +153,7 @@ public class GDALStore extends DataStore implements 
Aggregate, ResourceOnFileSys
         path       = connector.getStorageAs(Path.class);
         String url = connector.commit(String.class, GDALStoreProvider.NAME);
         if (location != null) {
-            url = Opener.toURL(location, path);
+            url = Opener.toURL(location, path, true);
         }
         Opener opener;
         opener = new Opener(provider, url, drivers);
@@ -275,11 +274,11 @@ public class GDALStore extends DataStore implements 
Aggregate, ResourceOnFileSys
      *       <abbr>GDAL</abbr> provides a {@code GDALCopyDatasetFiles} 
function for this purpose.
      *       That function is not yet used by Apache <abbr>SIS</abbr>.
      *
-     * @return files used by this resource, or an empty array if unknown.
+     * @return files used by this resource, or an empty value if unknown.
      * @throws DataStoreException if the list of files cannot be obtained.
      */
     @Override
-    public synchronized Path[] getComponentFiles() throws DataStoreException {
+    public synchronized Optional<FileSet> getFileSet() throws 
DataStoreException {
         final GDAL gdal = getProvider().GDAL();
         final List<String> files;
         try {
@@ -292,18 +291,23 @@ public class GDALStore extends DataStore implements 
Aggregate, ResourceOnFileSys
         } catch (Throwable e) {
             throw GDAL.propagate(e);
         }
-        if (files == null || files.isEmpty()) {
-            return (path != null) ? new Path[] {path} : new Path[0];
-        }
-        final var paths = new Path[files.size()];
-        for (int i=0; i < paths.length; i++) {
-            var item = Path.of(files.get(i));
-            if (path != null) {
-                item = path.resolveSibling(item);
+        final FileSet fs;
+        if (files != null && !files.isEmpty()) {
+            final var paths = new Path[files.size()];
+            for (int i=0; i < paths.length; i++) {
+                var item = Path.of(files.get(i));
+                if (path != null) {
+                    item = path.resolveSibling(item);
+                }
+                paths[i] = item;
             }
-            paths[i] = item;
+            fs = getDriver().new FileList(paths, path, location);
+        } else if (path != null) {
+            fs = new FileSet(path);
+        } else {
+            return Optional.empty();
         }
-        return paths;
+        return Optional.of(fs);
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java
 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java
index 4a2539745f..642e30d89d 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Opener.java
@@ -117,9 +117,10 @@ final class Opener implements Runnable {
      *
      * @param  location  URL to the file to open (mandatory).
      * @param  path      URL as a path on the file system, or {@code null} if 
none.
-     * @return <abbr>URL</abbr> for <var>GDAL</var>.
+     * @param  fallback  whether to use a fallback value if the URI is not 
recognized.
+     * @return <abbr>URL</abbr> for <var>GDAL</var>. May be {@code null} if 
unrecognized and no fallback is used.
      */
-    static String toURL(final URI location, final Path path) {
+    static String toURL(final URI location, final Path path, final boolean 
fallback) {
         String url;
         final String scheme = location.getScheme();
         if (path != null && "file".equalsIgnoreCase(scheme)) {
@@ -128,6 +129,8 @@ final class Opener implements Runnable {
             url = location.toString();
             if (scheme != null && 
VSICURL.contains(scheme.toLowerCase(Locale.US))) {
                 url = "/vsicurl/".concat(url);
+            } else if (!fallback) {
+                return null;
             }
         }
         return url;
@@ -147,7 +150,7 @@ final class Opener implements Runnable {
         String url;
         final URI location = connector.getStorageAs(URI.class);
         if (location != null) {
-            url = toURL(location, connector.getStorageAs(Path.class));
+            url = toURL(location, connector.getStorageAs(Path.class), true);
         } else {
             url = connector.getStorageAs(String.class);
         }
diff --git 
a/incubator/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java
 
b/incubator/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java
index d316abd1e4..78755043bd 100644
--- 
a/incubator/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java
+++ 
b/incubator/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java
@@ -16,7 +16,10 @@
  */
 package org.apache.sis.storage.gdal;
 
+import java.util.Collection;
 import java.nio.file.Path;
+import java.nio.file.Files;
+import java.io.IOException;
 import java.awt.image.DataBuffer;
 import java.awt.image.RenderedImage;
 import org.opengis.util.GenericName;
@@ -49,6 +52,11 @@ import static org.junit.jupiter.api.Assertions.*;
  * @author Quentin BIALOTA (Geomatys)
  */
 public final class GDALStoreTest {
+    /**
+     * Name of the test file.
+     */
+    private static final String FILENAME = "test.tiff";
+
     /**
      * Creates a new test case.
      */
@@ -72,7 +80,7 @@ public final class GDALStoreTest {
      * Returns the storage connector to the test file to use as input.
      */
     private static StorageConnector input() {
-        return new 
StorageConnector(GDALStoreTest.class.getResource("test.tiff"));
+        return new StorageConnector(GDALStoreTest.class.getResource(FILENAME));
     }
 
     /**
@@ -159,11 +167,54 @@ public final class GDALStoreTest {
             }
 
             // Check the file components
-            final Path[] paths = store.getComponentFiles();
-            assertEquals(1, paths.length);
-            assertEquals("test.tiff", paths[0].getFileName().toString());
+            final Collection<Path> paths = 
store.getFileSet().orElseThrow().getPaths();
+            assertEquals(1, paths.size());
+            assertEquals(FILENAME, 
paths.iterator().next().getFileName().toString());
         }
         assertTrue(foundGrid);
         assertTrue(foundBand);
     }
+
+    /**
+     * Tests {@link GDALStore#getFileSet()}.
+     *
+     * @throws DataStoreException if an error occurred while fetching the list 
of files.
+     * @throws IOException if an error occurred while copying the file.
+     */
+    @Test
+    public void testFileSet() throws DataStoreException, IOException {
+        final var provider = new GDALStoreProvider();
+        GDALStore.FileSet fileset;
+        try (GDALStore store = new GDALStore(provider, input())) {
+            fileset = store.getFileSet().orElseThrow();
+        }
+        final Path source = fileset.getPaths().iterator().next();
+        assertEquals(FILENAME, source.getFileName().toString());
+        /*
+         * Tests the copy operation in a temporary directory.
+         */
+        final Path dir = Files.createTempDirectory("sis-");
+        Path target = null;
+        try {
+            target = fileset.copy(dir);
+            assertEquals(dir, target.getParent());
+            assertEquals(FILENAME, target.getFileName().toString());
+            assertArrayEquals(Files.readAllBytes(source), 
Files.readAllBytes(target));
+            /*
+             * In order to test the delete operation, we need to open a new 
data store on the file
+             * that we just copied. Otherwise, `fileset.delete()` would delete 
the original file.
+             */
+            try (GDALStore store = new GDALStore(provider, new 
StorageConnector(target))) {
+                fileset = store.getFileSet().orElseThrow();
+            }
+            fileset.delete();
+            assertTrue(Files.notExists(target));
+            target = null;
+        } finally {
+            if (target != null) {
+                Files.deleteIfExists(target);
+            }
+            Files.deleteIfExists(dir);
+        }
+    }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java
 
b/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java
index 47167d894e..ea4db47c69 100644
--- 
a/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java
+++ 
b/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java
@@ -18,6 +18,7 @@ package org.apache.sis.storage.geopackage;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.List;
@@ -40,7 +41,6 @@ import org.apache.sis.storage.WritableAggregate;
 import org.apache.sis.storage.sql.DataAccess;
 import org.apache.sis.storage.sql.SQLStore;
 import org.apache.sis.storage.event.StoreListeners;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.metadata.sql.privy.ScriptRunner;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
@@ -76,7 +76,7 @@ import org.apache.sis.util.Workaround;
  *
  * @see <a href="https://www.geopackage.org/spec140/index.html";>OGC® 
GeoPackage Encoding Standard version 1.4.0</a>
  */
-public class GpkgStore extends SQLStore implements WritableAggregate, 
ResourceOnFileSystem {
+public class GpkgStore extends SQLStore implements WritableAggregate {
     /**
      * Type of data which for which {@code GpkgStore} will delegate the 
handling to the {@code SQLStore} parent.
      * A data type is a values of the {@value Content#DATA_TYPE} column of the 
{@value Content#TABLE_NAME} table.
@@ -87,7 +87,7 @@ public class GpkgStore extends SQLStore implements 
WritableAggregate, ResourceOn
      * Path to the SQLite file, or {@code null} if the store was opened with a 
{@link DataSource}.
      * This is for information purpose only.
      *
-     * @see #getComponentFiles()
+     * @see #getFileSet()
      */
     private final Path path;
 
@@ -154,21 +154,23 @@ public class GpkgStore extends SQLStore implements 
WritableAggregate, ResourceOn
     }
 
     /**
-     * Returns the path to the main file and its auxiliary files, or an empty 
array if unknown.
+     * Returns the path to the main file and its auxiliary files, or an empty 
value if unknown.
      *
-     * @return the Geopackage file and auxiliary files, or an empty array if 
unknown.
+     * @return the Geopackage file and auxiliary files, or an empty value if 
unknown.
      */
     @Override
-    public Path[] getComponentFiles() {
+    public Optional<FileSet> getFileSet() {
         if (path == null) {
-            return new Path[0];
+            return Optional.empty();
         }
+        final var paths = new ArrayList<Path>(3);
+        paths.add(path);
         final String filename = path.getFileName().toString();
-        return new Path[] {
-            path,
-            path.resolveSibling(filename.concat(Initializer.WAL_SUFFIX)),
-            path.resolveSibling(filename.concat(Initializer.SHM_SUFFIX))
-        };
+        for (String suffix : new String[] {Initializer.WAL_SUFFIX, 
Initializer.SHM_SUFFIX}) {
+            Path aux = path.resolveSibling(filename.concat(suffix));
+            if (Files.exists(aux)) paths.add(aux);
+        }
+        return Optional.of(new FileSet(paths));
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFRecordReader.java
 
b/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFRecordReader.java
index 246c3fdd8d..45ed42207a 100644
--- 
a/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFRecordReader.java
+++ 
b/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFRecordReader.java
@@ -45,7 +45,7 @@ public final class GSFRecordReader implements AutoCloseable {
 
     public GSFRecordReader(GSFStore store) throws DataStoreException {
         this.store = store;
-        this.file = store.getComponentFiles()[0];
+        this.file = 
store.getFileSet().orElseThrow().getPaths().iterator().next();
         this.gsf = store.getProvider().GSF();
         this.arena = Arena.ofShared();
 
diff --git 
a/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFStore.java
 
b/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFStore.java
index bb70c1158d..644c74445c 100644
--- 
a/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFStore.java
+++ 
b/incubator/src/org.apache.sis.storage.gsf/main/org/apache/sis/storage/gsf/GSFStore.java
@@ -24,7 +24,6 @@ import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.base.MetadataBuilder;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.storage.base.URIDataStore;
 import org.opengis.metadata.Metadata;
 import org.opengis.parameter.ParameterValueGroup;
@@ -33,7 +32,7 @@ import org.opengis.parameter.ParameterValueGroup;
  *
  * @author Johann Sorel (Geomatys)
  */
-public final class GSFStore extends DataStore implements ResourceOnFileSystem {
+public final class GSFStore extends DataStore {
     /**
      * The {@link GSFStoreProvider#LOCATION} parameter value, or {@code null} 
if none.
      *
@@ -43,7 +42,7 @@ public final class GSFStore extends DataStore implements 
ResourceOnFileSystem {
     /**
      * Path of the file opened by this data store, or {@code null} if none.
      *
-     * @see #getComponentFiles()
+     * @see #getFileSet()
      */
     private final Path path;
     /**
@@ -92,8 +91,8 @@ public final class GSFStore extends DataStore implements 
ResourceOnFileSystem {
     }
 
     @Override
-    public Path[] getComponentFiles() throws DataStoreException {
-        return new Path[]{path};
+    public Optional<FileSet> getFileSet() throws DataStoreException {
+        return Optional.of(new FileSet(path));
     }
 
     @Override
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
index 2457bb271c..a3e36941cd 100644
--- 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
@@ -90,7 +90,6 @@ import org.apache.sis.storage.Query;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.UnsupportedQueryException;
 import org.apache.sis.storage.WritableFeatureSet;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.storage.shapefile.cpg.CpgFiles;
 import org.apache.sis.storage.shapefile.dbf.DBFField;
 import org.apache.sis.storage.shapefile.dbf.DBFHeader;
@@ -127,7 +126,7 @@ import org.opengis.filter.ValueReference;
  *
  * @author Johann Sorel (Geomatys)
  */
-public final class ShapefileStore extends DataStore implements 
WritableFeatureSet, ResourceOnFileSystem {
+public final class ShapefileStore extends DataStore implements 
WritableFeatureSet {
 
     private static final String GEOMETRY_NAME = "geometry";
     private static final Logger LOGGER = 
Logger.getLogger("org.apache.sis.storage.shapefile");
@@ -270,11 +269,11 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
      * {@inheritDoc }
      */
     @Override
-    public Path[] getComponentFiles() throws DataStoreException {
-        return featureSetView.getComponentFiles();
+    public Optional<FileSet> getFileSet() throws DataStoreException {
+        return featureSetView.getFileSet();
     }
 
-    private class AsFeatureSet extends AbstractFeatureSet implements 
WritableFeatureSet, ResourceOnFileSystem {
+    private class AsFeatureSet extends AbstractFeatureSet implements 
WritableFeatureSet {
 
         private final Rectangle2D.Double filter;
         private final Set<String> dbfProperties;
@@ -853,8 +852,8 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
         }
 
         @Override
-        public Path[] getComponentFiles() throws DataStoreException {
-            final List<Path> paths = new ArrayList<>();
+        public Optional<FileSet> getFileSet() throws DataStoreException {
+            final var paths = new ArrayList<Path>(5);
             final Path shp = files.shpFile;
             final Path shx = files.getShx(false);
             final Path dbf = files.getDbf(false);
@@ -865,7 +864,7 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
             if (dbf != null && Files.exists(dbf)) paths.add(dbf);
             if (prj != null && Files.exists(prj)) paths.add(prj);
             if (cpg != null && Files.exists(cpg)) paths.add(cpg);
-            return paths.toArray(Path[]::new);
+            return Optional.of(new FileSet(paths));
         }
     }
 
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
index ec293c91fb..b5f642304f 100644
--- 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
@@ -180,13 +180,13 @@ public class ShapefileStoreTest {
     public void testFiles() throws URISyntaxException, DataStoreException {
         final URL url = 
ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp");
         try (final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()))) {
-            Path[] componentFiles = store.getComponentFiles();
-            assertEquals(5, componentFiles.length);
-            assertTrue(componentFiles[0].toString().endsWith("point.shp"));
-            assertTrue(componentFiles[1].toString().endsWith("point.shx"));
-            assertTrue(componentFiles[2].toString().endsWith("point.dbf"));
-            assertTrue(componentFiles[3].toString().endsWith("point.prj"));
-            assertTrue(componentFiles[4].toString().endsWith("point.cpg"));
+            Iterator<Path> componentFiles = 
store.getFileSet().orElseThrow().getPaths().iterator();
+            assertTrue(componentFiles.next().toString().endsWith("point.shp"));
+            assertTrue(componentFiles.next().toString().endsWith("point.shx"));
+            assertTrue(componentFiles.next().toString().endsWith("point.dbf"));
+            assertTrue(componentFiles.next().toString().endsWith("point.prj"));
+            assertTrue(componentFiles.next().toString().endsWith("point.cpg"));
+            assertFalse(componentFiles.hasNext());
         }
     }
 
@@ -198,8 +198,7 @@ public class ShapefileStoreTest {
         final Path temp = folder.resolve("test.shp");
         final String name = temp.getFileName().toString().split("\\.")[0];
         try (final ShapefileStore store = new ShapefileStore(temp)) {
-            Path[] componentFiles = store.getComponentFiles();
-            assertEquals(0, componentFiles.length);
+            assertTrue(store.getFileSet().orElseThrow().getPaths().isEmpty());
 
             {//create type
                 final FeatureType type = createType();
@@ -207,13 +206,13 @@ public class ShapefileStoreTest {
             }
 
             {//check files have been created
-                componentFiles = store.getComponentFiles();
-                assertEquals(5, componentFiles.length);
-                assertTrue(componentFiles[0].toString().endsWith(name+".shp"));
-                assertTrue(componentFiles[1].toString().endsWith(name+".shx"));
-                assertTrue(componentFiles[2].toString().endsWith(name+".dbf"));
-                assertTrue( 
componentFiles[3].toString().endsWith(name+".prj"));
-                assertTrue(componentFiles[4].toString().endsWith(name+".cpg"));
+                Iterator<Path> componentFiles = 
store.getFileSet().orElseThrow().getPaths().iterator();
+                
assertTrue(componentFiles.next().toString().endsWith(name+".shp"));
+                
assertTrue(componentFiles.next().toString().endsWith(name+".shx"));
+                
assertTrue(componentFiles.next().toString().endsWith(name+".dbf"));
+                
assertTrue(componentFiles.next().toString().endsWith(name+".prj"));
+                
assertTrue(componentFiles.next().toString().endsWith(name+".cpg"));
+                assertFalse(componentFiles.hasNext());
             }
 
             {// check created type
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java
index 61a36be73e..fa26f7162d 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java
@@ -19,6 +19,7 @@ package org.apache.sis.gui.dataset;
 import java.awt.Desktop;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.io.File;
 import java.nio.file.Path;
 import java.net.URL;
@@ -32,7 +33,6 @@ import javafx.scene.input.ClipboardContent;
 import org.apache.sis.gui.internal.ExceptionReporter;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.Resource;
-import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.io.stream.IOUtilities;
 
@@ -141,26 +141,27 @@ final class PathAction implements 
EventHandler<ActionEvent> {
          * This list of files will usually be ignored and only the `file` text 
will be pasted,
          * but it depends on the application where files will be pasted.
          */
-        List<File> files = null;
-        if (resource instanceof ResourceOnFileSystem) try {
-            final Path[] components = ((ResourceOnFileSystem) 
resource).getComponentFiles();
-            if (components != null) {
-                files = new ArrayList<>(components.length);
-                for (final Path p : components) try {
-                    if (p != null) files.add(p.toFile());
-                } catch (UnsupportedOperationException e) {
-                    // Ignore and try to add other components.
-                }
-            }
+        Collection<Path> components = null;
+        try {
+            components = 
resource.getFileSet().map(Resource.FileSet::getPaths).orElse(null);
         } catch (DataStoreException e) {
             ResourceTree.unexpectedException("copy", e);
+        }
+        List<File> files = null;
+        if (components != null) {
+            files = new ArrayList<>(components.size());
+            for (final Path p : components) try {
+                if (p != null) files.add(p.toFile());
+            } catch (UnsupportedOperationException e) {
+                // Ignore and try to add other components.
+            }
         } else if (file instanceof File) {
             files = List.of((File) file);
         }
         /*
          * Put in the clipboard all information that we could get.
          */
-        final ClipboardContent content = new ClipboardContent();
+        final var content = new ClipboardContent();
         content.putString(file.toString());
         if (files != null) content.putFiles(files);
         if (uri   != null) content.putUrl(uri.toString());

Reply via email to