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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new f75a439056 WebDAV VFS and metadata type #7123 (#7124)
f75a439056 is described below

commit f75a439056c138a6a6636140d657ff5149152e41
Author: Bart Maertens <[email protected]>
AuthorDate: Mon May 18 11:33:14 2026 +0000

    WebDAV VFS and metadata type #7123 (#7124)
    
    * WebDAV: expose metadata name in FileName URIs (logical scheme)
    
    HopWebDavConnectionFileName wraps Webdav4FileName so getURI/getRootURI/
    getFriendlyURI use the connection metadata name (e.g. nextcloud:///path)
    while wire host/path stay on webdav4 for HTTP.
    
    Undo: git revert this commit (files: HopWebDavLogicalUris.java,
    HopWebDavConnectionFileName.java, parser + tests).
    
    Co-authored-by: Cursor <[email protected]>
    
    * WebDAV: wrap listed/resolved files with logical connection URI
    
    Webdav4FileSystem.createFile was still attaching plain Webdav4FileName
    from PROPFIND/listing. Use HopWebDavConnectionFileSystem to wrap every
    Webdav4FileName so HopVfsFileDialog and getURI() show the metadata scheme.
    
    Undo: git revert this commit.
    
    * WebDAV: logical URIs after PROPFIND list (Get File Names)
    
    doListChildrenResolved returns FileObjects that bypass createFile;
    subclass Webdav4FileObject to re-resolve children with 
HopWebDavConnectionFileName.
    
    Undo: git revert this commit.
    
    * WebDAV: logical getScheme + resolveName URI shape for child listing
    
    DefaultFileSystemManager.resolveName picks the provider from 
parent.getScheme();
    HopWebDavConnectionFileName still reported webdav4 so PROPFIND child 
resolution
    used the global Webdav4FileProvider. Return the connection name from 
getScheme,
    keep wireScheme for Webdav4FileName children, and parse resolveName-style 
URIs
    (rootURI + full wire path) into the correct synthetic webdav4 URL.
    
    Undo: git revert this commit.
    
    * initial metadata type for webdav
    
    * initial webdav metadata type working version
    
    * webdav doc and i18n updates
    
    * cleanup #7123
    
    ---------
---
 assemblies/plugins/pom.xml                         |   6 +
 core/pom.xml                                       |   4 +
 .../hop/metadata/api/HopMetadataPropertyType.java  |   1 +
 docs/hop-user-manual/modules/ROOT/nav.adoc         |   1 +
 .../modules/ROOT/pages/metadata-types/index.adoc   |   1 +
 .../pages/metadata-types/webdav-connection.adoc    |  58 ++++++
 docs/hop-user-manual/modules/ROOT/pages/vfs.adoc   |  11 +-
 lib/pom.xml                                        |   5 +
 plugins/tech/pom.xml                               |   1 +
 plugins/tech/{ => webdav}/pom.xml                  |  50 +++--
 plugins/tech/webdav/src/assembly/assembly.xml      |  53 +++++
 .../hop/vfs/webdav/HopRawWebDavFileSystem.java     |  38 ++++
 .../hop/vfs/webdav/HopWebDavConnectionAuth.java    |  44 ++++
 .../apache/hop/vfs/webdav/HopWebDavFileName.java   |  91 ++++++++
 .../hop/vfs/webdav/HopWebDavFileNameParser.java    |  67 ++++++
 .../apache/hop/vfs/webdav/HopWebDavFileObject.java |  91 ++++++++
 .../hop/vfs/webdav/HopWebDavFileProvider.java      | 106 ++++++++++
 .../apache/hop/vfs/webdav/HopWebDavFileSystem.java | 141 +++++++++++++
 .../hop/vfs/webdav/HopWebDavLogicalUris.java       |  95 +++++++++
 .../hop/vfs/webdav/HopWebDavWireBootstrap.java     |  79 +++++++
 .../apache/hop/vfs/webdav/HopWebDavWireNames.java  |  51 +++++
 .../hop/vfs/webdav/WebDavSecureVfsPlugin.java      |  51 +++++
 .../org/apache/hop/vfs/webdav/WebDavVfsPlugin.java |  69 ++++++
 .../hop/vfs/webdav/metadata/WebDavConnection.java  |  66 ++++++
 .../webdav/metadata/WebDavConnectionEditor.java    | 232 +++++++++++++++++++++
 .../metadata/messages/messages_en_US.properties    |  26 +++
 plugins/tech/webdav/src/main/resources/version.xml |  20 ++
 .../vfs/webdav/HopWebDavFileNameParserTest.java    |  67 ++++++
 .../hop/vfs/webdav/HopWebDavFileNameTest.java      |  55 +++++
 .../hop/vfs/webdav/HopWebDavLogicalUrisTest.java   |  81 +++++++
 .../apache/hop/ui/core/vfs/HopVfsFileDialog.java   |   2 +-
 31 files changed, 1637 insertions(+), 26 deletions(-)

diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml
index 940320ef30..3e7e6fbe81 100644
--- a/assemblies/plugins/pom.xml
+++ b/assemblies/plugins/pom.xml
@@ -811,6 +811,12 @@
             <version>${project.version}</version>
             <type>zip</type>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-tech-webdav</artifactId>
+            <version>${project.version}</version>
+            <type>zip</type>
+        </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
             <artifactId>hop-transform-abort</artifactId>
diff --git a/core/pom.xml b/core/pom.xml
index fb80755901..afe26470d8 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -221,6 +221,10 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-vfs2-jackrabbit2</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpcore</artifactId>
diff --git 
a/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java 
b/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java
index cf0e62e867..bc72cd8cd5 100644
--- 
a/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java
+++ 
b/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java
@@ -100,6 +100,7 @@ public enum HopMetadataPropertyType {
   VFS_AZURE_CONNECTION,
   VFS_MINIO_CONNECTION,
   VFS_S3_CONNECTION,
+  VFS_WEBDAV_CONNECTION,
 
   // DATA STREAM
   DATA_STREAM,
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc 
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index 40fedf4405..a741cefcfc 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -461,6 +461,7 @@ under the License.
 *** 
xref:metadata-types/variable-resolver/pipeline-variable-resolver.adoc[Pipeline 
variable resolver]
 ** xref:metadata-types/static-schema-definition.adoc[Static Schema Definition]
 ** xref:hop-server/web-service.adoc[Web Service]
+** xref:metadata-types/webdav-connection.adoc[WebDAV Connection]
 ** xref:metadata-types/workflow-log.adoc[Workflow Log]
 ** xref:metadata-types/workflow-run-config.adoc[Workflow Run Configuration]
 * xref:password/passwords.adoc[Passwords]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc
index 505eff74ce..2252c38e26 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc
@@ -48,6 +48,7 @@ xref:metadata-types/data-stream/data-stream.adoc[Data 
Stream]: A Data Stream can
 * xref:metadata-types/mongodb-connection.adoc[MongoDB Connection]: Describes a 
MongoDB connection
 * xref:metadata-types/mail-server-connection.adoc[Mail Server Connection]: 
Describes a mail server connection
 * xref:metadata-types/minio-connection.adoc[Minio (S3) Connection]: Connect to 
one or more S3 endpoints using a Minio client
+* xref:metadata-types/webdav-connection.adoc[WebDAV Connection]: Named WebDAV 
or WebDAV-over-HTTPS endpoint used as a VFS scheme in file paths
 * xref:metadata-types/neo4j/neo4j-connection.adoc[Neo4j Connection]: A shared 
connection to a Neo4j server
 * xref:metadata-types/neo4j/neo4j-graphmodel.adoc[Neo4j Graph Model]: 
Description of the nodes, relationships, indexes, ... of a Neo4j graph
 * xref:metadata-types/partition-schema.adoc[Partition Schema]: Describes a 
partition schema
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/webdav-connection.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/webdav-connection.adoc
new file mode 100644
index 0000000000..86f6ef70a4
--- /dev/null
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/webdav-connection.adoc
@@ -0,0 +1,58 @@
+////
+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.
+////
+:imagesdir: ../../assets/images/
+:page-pagination:
+:description: Register a named WebDAV (HTTP/WebDAV or HTTPS) endpoint for use 
in Hop VFS paths.
+
+= WebDAV Connection
+
+== Description
+
+This metadata type registers a **named** WebDAV endpoint in your project.
+Hop exposes each connection under the **connection name** as a VFS scheme, so 
you reference files with:
+
+`+myConnectionName:///path/under/webdav/root+`
+
+The **WebDAV root URL** in the metadata must be a full Apache Commons VFS 
WebDAV URL (`webdav4://` for HTTP or `webdav4s://` for HTTPS), including the 
path to your DAV root (for example a Nextcloud folder root such as 
`+/remote.php/dav/files/username/+`).
+
+Credentials are configured in this metadata object and applied through 
VFS—they are **not** embedded in Hop file paths.
+
+For the underlying `webdav4` / `webdav4s` URI syntax and options, see 
xref:vfs.adoc[VFS] (WebDAV section).
+
+IMPORTANT: Many hosted servers redirect HTTP to HTTPS with `301`. Use 
**`webdav4s://`** in **WebDAV root URL** when the server expects TLS, otherwise 
listing or type detection can fail while desktop clients (that follow redirects 
for WebDAV methods) may still work.
+
+NOTE: **Username**, **password**, and **WebDAV root URL** support variable 
substitution. Passwords can be stored encrypted in metadata; Hop decrypts them 
when resolving the connection.
+
+== Options
+
+[options="header"]
+|===
+|Option |Description
+|Name| Name of this connection; used as the URI scheme in paths 
(`+name:///...+`)
+|Description| Optional longer description
+|WebDAV root URL| Full URL including scheme `webdav4://` or `webdav4s://`, 
host, optional port, and DAV path (often with a trailing slash). Example: 
`+webdav4s://cloud.example.com/remote.php/dav/files/admin/+`
+|Username| User name for authentication (optional if the server allows 
anonymous access)
+|Password| Password or application password; supports variables and encrypted 
values
+|Follow HTTP redirects| Passed to the HTTP client (see VFS/WebDAV provider 
behavior for redirect limits on WebDAV methods)
+|Preemptive basic authentication| Sends credentials proactively for servers 
that require it
+|===
+
+== Tips
+
+* To verify a connection, open **File → Open** (or any file dialog) and browse 
`+YourConnectionName:///+` or drill into a subfolder path.
+* Prefer **`webdav4s://`** for Nextcloud and similar hosts that enforce HTTPS.
+* Pick a **Name** that does not clash with 
https://commons.apache.org/proper/commons-vfs/filesystems.html[built-in VFS 
schemes] (`file`, `ftp`, `http`, …) or other registered plugins (`s3`, `azure`, 
`gs`, etc.).
diff --git a/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc
index 6ad706632a..846ff157da 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc
@@ -43,6 +43,7 @@ Click the File system name to access more detailed file 
system documentation.
 |xref:vfs/google-cloud-storage-vfs.adoc[Google Cloud Storage]|Provides access 
to Google Cloud Storage buckets|`gs://`
 |xref:vfs/google-drive-vfs.adoc[Google Drive]|Provides access to Google Drive 
folders|`googledrive://`
 |xref:metadata-types/minio-connection.adoc[Minio connection]|Provides access 
to S3 endpoints using a Minio client|`any://`
+|xref:metadata-types/webdav-connection.adoc[WebDAV Connection]|Provides access 
to WebDAV servers via a named connection (metadata)|`+connectionName://+`
 |===
 
 == Apache VFS File System Types
@@ -286,11 +287,12 @@ Examples
 //
 // WebDAV
 //
-|WebDAV|Provides access to files on a WebDAV server through the modules 
`commons-vfs2-jackrabbit1` and `commons-vfs2-jackrabbit2`.
+|WebDAV|Provides access through `commons-vfs2-jackrabbit2`. Hop registers 
providers in 
link:https://github.com/apache/hop/blob/main/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java[HopVfs.java].
 a|
-URI Format
+URI Format — Apache Commons VFS 2 schemes (not legacy `webdav://`):
 
-`+webdav://[ username[: password]@] hostname[: port][ absolute-path]+`
+* `+webdav4://[ username[: password]@] hostname[: port][ absolute-path]+` 
(HTTP)
+* `+webdav4s://[ username[: password]@] hostname[: port][ absolute-path]+` 
(HTTPS)
 
 File System Options
 
@@ -300,7 +302,8 @@ If not set the user name used to authenticate will be used.
 
 Examples
 
-* `+webdav://somehost:8080/dist+`
+* `+webdav4://someuser@somehost:8080/remote.php/dav/files/someuser/+`
+* `+webdav4s://[email protected]/remote.php/dav/files/someuser/+`
 
 //
 // Zip
diff --git a/lib/pom.xml b/lib/pom.xml
index ac748030ad..5d9c4df7f7 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -655,6 +655,11 @@
                 <artifactId>commons-vfs2</artifactId>
                 <version>${commons-vfs2.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-vfs2-jackrabbit2</artifactId>
+                <version>${commons-vfs2.version}</version>
+            </dependency>
             <dependency>
                 <groupId>commons-net</groupId>
                 <artifactId>commons-net</artifactId>
diff --git a/plugins/tech/pom.xml b/plugins/tech/pom.xml
index 6904c55e10..4d78da480d 100644
--- a/plugins/tech/pom.xml
+++ b/plugins/tech/pom.xml
@@ -44,5 +44,6 @@
         <module>parquet</module>
         <module>salesforce</module>
         <module>vault</module>
+        <module>webdav</module>
     </modules>
 </project>
diff --git a/plugins/tech/pom.xml b/plugins/tech/webdav/pom.xml
similarity index 55%
copy from plugins/tech/pom.xml
copy to plugins/tech/webdav/pom.xml
index 6904c55e10..998a5c46d2 100644
--- a/plugins/tech/pom.xml
+++ b/plugins/tech/webdav/pom.xml
@@ -20,29 +20,37 @@
 
     <parent>
         <groupId>org.apache.hop</groupId>
-        <artifactId>hop-plugins</artifactId>
+        <artifactId>hop-plugins-tech</artifactId>
         <version>2.18.0-SNAPSHOT</version>
     </parent>
 
-    <artifactId>hop-plugins-tech</artifactId>
-    <packaging>pom</packaging>
-    <name>Hop Plugins Technology</name>
+    <artifactId>hop-tech-webdav</artifactId>
+    <packaging>jar</packaging>
+    <name>Hop Plugins Technology WebDAV</name>
 
-    <modules>
-        <module>arrow</module>
-        <module>avro</module>
-        <module>aws</module>
-        <module>azure</module>
-        <module>cassandra</module>
-        <module>databricks</module>
-        <module>dropbox</module>
-        <module>elastic</module>
-        <module>google</module>
-        <module>minio</module>
-        <module>neo4j</module>
-        <module>opensearch</module>
-        <module>parquet</module>
-        <module>salesforce</module>
-        <module>vault</module>
-    </modules>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.hop</groupId>
+                <artifactId>hop-libs</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-vfs2-jackrabbit2</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
 </project>
diff --git a/plugins/tech/webdav/src/assembly/assembly.xml 
b/plugins/tech/webdav/src/assembly/assembly.xml
new file mode 100644
index 0000000000..a96a757132
--- /dev/null
+++ b/plugins/tech/webdav/src/assembly/assembly.xml
@@ -0,0 +1,53 @@
+<!--
+  ~ 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.
+  -->
+
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0";
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+    xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 
http://maven.apache.org/xsd/assembly-2.2.0.xsd";>
+    <id>hop-tech-webdav</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <baseDirectory>.</baseDirectory>
+    <files>
+        <file>
+            <source>${project.basedir}/src/main/resources/version.xml</source>
+            <outputDirectory>plugins/tech/webdav</outputDirectory>
+            <filtered>true</filtered>
+        </file>
+    </files>
+
+    <dependencySets>
+        <dependencySet>
+            <includes>
+                <include>org.apache.hop:hop-tech-webdav:jar</include>
+            </includes>
+            <outputDirectory>plugins/tech/webdav</outputDirectory>
+        </dependencySet>
+        <dependencySet>
+            <scope>runtime</scope>
+            <excludes>
+                <exclude>org.apache.hop:hop-tech-webdav:jar</exclude>
+                <exclude>org.apache.commons:commons-lang3:jar</exclude>
+                <exclude>commons-io:commons-io:jar</exclude>
+                <exclude>org.apache.commons:commons-vfs2:jar</exclude>
+                
<exclude>org.apache.commons:commons-vfs2-jackrabbit2:jar</exclude>
+            </excludes>
+            <outputDirectory>plugins/tech/webdav/lib</outputDirectory>
+        </dependencySet>
+    </dependencySets>
+</assembly>
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopRawWebDavFileSystem.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopRawWebDavFileSystem.java
new file mode 100644
index 0000000000..05d817f0b9
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopRawWebDavFileSystem.java
@@ -0,0 +1,38 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystem;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.protocol.HttpClientContext;
+
+/**
+ * Subclass solely to expose {@link Webdav4FileSystem}'s protected constructor 
from application
+ * code.
+ */
+final class HopRawWebDavFileSystem extends Webdav4FileSystem {
+
+  HopRawWebDavFileSystem(
+      FileName rootName,
+      FileSystemOptions fileSystemOptions,
+      HttpClient httpClient,
+      HttpClientContext httpClientContext) {
+    super(rootName, fileSystemOptions, httpClient, httpClientContext);
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavConnectionAuth.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavConnectionAuth.java
new file mode 100644
index 0000000000..94564354f5
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavConnectionAuth.java
@@ -0,0 +1,44 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.auth.StaticUserAuthenticator;
+import org.apache.commons.vfs2.impl.DefaultFileSystemConfigBuilder;
+import org.apache.commons.vfs2.provider.http4.Http4FileSystemConfigBuilder;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+
+final class HopWebDavConnectionAuth {
+
+  private HopWebDavConnectionAuth() {}
+
+  static void apply(FileSystemOptions opts, IVariables variables, 
WebDavConnection meta) {
+    String user = Const.NVL(variables.resolve(meta.getUsername()), "");
+    String pass = Const.NVL(Utils.resolvePassword(variables, 
meta.getPassword()), "");
+    if (StringUtils.isNotEmpty(user)) {
+      DefaultFileSystemConfigBuilder.getInstance()
+          .setUserAuthenticator(opts, new StaticUserAuthenticator(null, user, 
pass));
+    }
+    Http4FileSystemConfigBuilder http4 = 
Http4FileSystemConfigBuilder.getInstance();
+    http4.setFollowRedirect(opts, meta.isFollowRedirects());
+    http4.setPreemptiveAuth(opts, meta.isPreemptiveAuth());
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileName.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileName.java
new file mode 100644
index 0000000000..add3e3b988
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileName.java
@@ -0,0 +1,91 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.provider.AbstractFileName;
+
+public final class HopWebDavFileName extends AbstractFileName {
+
+  public HopWebDavFileName(String scheme, String path, FileType type) {
+    super(scheme, normalizeStoredPath(path, type), type);
+  }
+
+  public static HopWebDavFileName buildChild(
+      HopWebDavFileName parent, String baseName, FileType childType) {
+    String p = parent.getPath();
+    String childPath;
+    if (p == null || p.isEmpty() || "/".equals(p)) {
+      childPath = "/" + baseName;
+    } else if (p.endsWith("/")) {
+      childPath = p + baseName;
+    } else {
+      childPath = p + "/" + baseName;
+    }
+    return new HopWebDavFileName(parent.getScheme(), childPath, childType);
+  }
+
+  private static String normalizeStoredPath(String path, FileType type) {
+    String p = StringUtils.defaultString(path);
+    if (p.isEmpty() || "/".equals(p)) {
+      return "/";
+    }
+    if (!p.startsWith("/")) {
+      p = "/" + p;
+    }
+    if (type == FileType.FOLDER && p.length() > 1 && !p.endsWith("/")) {
+      p = p + "/";
+    }
+    return p;
+  }
+
+  @Override
+  public FileName createName(String absPath, FileType fileType) {
+    return new HopWebDavFileName(getScheme(), absPath, fileType);
+  }
+
+  @Override
+  public String getURI() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(getScheme()).append(":///");
+    String p = getPath();
+    if (p == null || p.isEmpty() || "/".equals(p)) {
+      return sb.toString();
+    }
+    String suffix = p.startsWith("/") ? p.substring(1) : p;
+    sb.append(suffix);
+    return sb.toString();
+  }
+
+  @Override
+  public String getRootURI() {
+    return getScheme() + ":///";
+  }
+
+  @Override
+  public String getFriendlyURI() {
+    return getURI();
+  }
+
+  @Override
+  protected void appendRootUri(StringBuilder buffer, boolean addPassword) {
+    buffer.append(getScheme());
+    buffer.append(":///");
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParser.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParser.java
new file mode 100644
index 0000000000..2b0fb3aea9
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParser.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.provider.AbstractFileNameParser;
+import org.apache.commons.vfs2.provider.VfsComponentContext;
+import org.apache.hop.core.Const;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+
+final class HopWebDavFileNameParser extends AbstractFileNameParser {
+
+  private final WebDavConnection meta;
+
+  HopWebDavFileNameParser(WebDavConnection meta) {
+    this.meta = meta;
+  }
+
+  @Override
+  public FileName parseUri(VfsComponentContext context, FileName base, String 
uri)
+      throws FileSystemException {
+    String conn = Const.NVL(meta.getName(), "").trim();
+    if (StringUtils.isEmpty(conn)) {
+      throw new FileSystemException("WebDAV connection has no name");
+    }
+    int c = uri.indexOf(':');
+    if (c < 0) {
+      throw new FileSystemException("Missing URI scheme: " + uri);
+    }
+    String scheme = uri.substring(0, c);
+    if (!scheme.equals(conn)) {
+      throw new FileSystemException(
+          "URI scheme must match WebDAV connection name \"" + conn + "\", got: 
" + scheme);
+    }
+    String pathPart = HopWebDavLogicalUris.rawPathFromUri(uri);
+    String path = pathPart.isEmpty() || "/".equals(pathPart) ? "/" : pathPart;
+    FileType type = inferType(uri, path);
+    return new HopWebDavFileName(scheme, path, type);
+  }
+
+  private static FileType inferType(String uri, String path) {
+    if (uri.endsWith("/")) {
+      return FileType.FOLDER;
+    }
+    if ("/".equals(path) || (path.length() > 1 && path.endsWith("/"))) {
+      return FileType.FOLDER;
+    }
+    return FileType.FILE;
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileObject.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileObject.java
new file mode 100644
index 0000000000..5ee6405685
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileObject.java
@@ -0,0 +1,91 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.provider.DelegateFileObject;
+
+/**
+ * Presents {@link HopWebDavFileName} to callers while delegating operations 
to wire WebDAV4.
+ *
+ * <p>Stock {@code DelegateFileObject} lists only base names; VFS then calls 
{@code resolveFile} for
+ * each child, repeating wire work already done during the directory listing. 
Returning resolved
+ * children reuses the wire {@link FileObject}s from that listing.
+ *
+ * <p>Children produced by {@link #doListChildrenResolved()} carry the wire 
{@link FileType} from
+ * the PROPFIND so {@link #getType()} / {@link #exists()} need not hit the 
wire again (Hop's {@code
+ * FileInputList} may call both during the same scan).
+ */
+final class HopWebDavFileObject extends 
DelegateFileObject<HopWebDavFileSystem> {
+
+  /** When non-null, DAV listing already resolved type; cleared on {@link 
#refresh()}. */
+  private volatile FileType listingTypeHint;
+
+  HopWebDavFileObject(HopWebDavFileName name, HopWebDavFileSystem fs, 
FileObject wireDelegate)
+      throws FileSystemException {
+    super(name, fs, wireDelegate);
+  }
+
+  HopWebDavFileObject(
+      HopWebDavFileName name,
+      HopWebDavFileSystem fs,
+      FileObject wireDelegate,
+      FileType listingTypeHint)
+      throws FileSystemException {
+    super(name, fs, wireDelegate);
+    this.listingTypeHint = listingTypeHint;
+  }
+
+  @Override
+  protected FileType doGetType() throws FileSystemException {
+    FileType hint = listingTypeHint;
+    if (hint != null) {
+      return hint;
+    }
+    return super.doGetType();
+  }
+
+  @Override
+  public void refresh() throws FileSystemException {
+    listingTypeHint = null;
+    super.refresh();
+  }
+
+  @Override
+  protected FileObject[] doListChildrenResolved() throws Exception {
+    FileObject wireDir = getDelegateFile();
+    if (wireDir == null) {
+      return null;
+    }
+    FileObject[] wireChildren = wireDir.getChildren();
+    HopWebDavFileSystem fs = getAbstractFileSystem();
+    HopWebDavFileName parentName = (HopWebDavFileName) getName();
+    FileObject[] out = new FileObject[wireChildren.length];
+    for (int i = 0; i < wireChildren.length; i++) {
+      FileObject w = wireChildren[i];
+      FileType wt = w.getType();
+      HopWebDavFileName childName =
+          HopWebDavFileName.buildChild(parentName, w.getName().getBaseName(), 
wt);
+      HopWebDavFileObject child = new HopWebDavFileObject(childName, fs, w, 
wt);
+      fs.rememberListedChild(child);
+      out[i] = child;
+    }
+    return out;
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileProvider.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileProvider.java
new file mode 100644
index 0000000000..8e4df7223d
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileProvider.java
@@ -0,0 +1,106 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.Capability;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystem;
+import org.apache.commons.vfs2.FileSystemConfigBuilder;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider;
+import org.apache.commons.vfs2.provider.FileNameParser;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileName;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileNameParser;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileProvider;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystem;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystemConfigBuilder;
+import org.apache.commons.vfs2.provider.webdav4s.Webdav4sFileNameParser;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+
+public class HopWebDavFileProvider extends AbstractOriginatingFileProvider {
+
+  private static final Collection<Capability> CAPABILITIES;
+
+  static {
+    Collection<Capability> c = new ArrayList<>(new 
Webdav4FileProvider().getCapabilities());
+    CAPABILITIES = Collections.unmodifiableCollection(c);
+  }
+
+  private final IVariables variables;
+  private final WebDavConnection meta;
+
+  public HopWebDavFileProvider(IVariables variables, WebDavConnection meta) {
+    this.variables = variables;
+    this.meta = meta;
+    setFileNameParser(new HopWebDavFileNameParser(meta));
+  }
+
+  @Override
+  public Collection<Capability> getCapabilities() {
+    return CAPABILITIES;
+  }
+
+  @Override
+  public FileSystemConfigBuilder getConfigBuilder() {
+    return Webdav4FileSystemConfigBuilder.getInstance();
+  }
+
+  @Override
+  protected FileSystem doCreateFileSystem(FileName rootName, FileSystemOptions 
fileSystemOptions)
+      throws FileSystemException {
+    FileSystemOptions opts =
+        fileSystemOptions != null
+            ? (FileSystemOptions) fileSystemOptions.clone()
+            : new FileSystemOptions();
+    HopWebDavConnectionAuth.apply(opts, variables, meta);
+    Webdav4FileSystemConfigBuilder httpCfg = 
Webdav4FileSystemConfigBuilder.getInstance();
+    httpCfg.setMaxConnectionsPerHost(opts, 32);
+    httpCfg.setMaxTotalConnections(opts, 64);
+    httpCfg.setKeepAlive(opts, true);
+
+    String rootUrl = Const.NVL(variables.resolve(meta.getRootUrl()), 
"").trim();
+    if (StringUtils.isEmpty(rootUrl)) {
+      throw new FileSystemException("WebDAV connection \"" + meta.getName() + 
"\" has no rootUrl");
+    }
+    String lower = rootUrl.toLowerCase();
+    if (!lower.startsWith("webdav4://") && !lower.startsWith("webdav4s://")) {
+      throw new FileSystemException(
+          "WebDAV rootUrl must start with webdav4:// or webdav4s://, got: " + 
rootUrl);
+    }
+    FileNameParser wireParser =
+        lower.startsWith("webdav4s://")
+            ? Webdav4sFileNameParser.getInstance()
+            : Webdav4FileNameParser.getInstance();
+    FileName wireRootParsed = wireParser.parseUri(getContext(), null, rootUrl);
+    Webdav4FileName wireRoot = HopWebDavWireNames.asWebdav4(wireRootParsed);
+
+    Webdav4FileSystem wireFs = HopWebDavWireBootstrap.create(opts, wireRoot);
+    // Wire FS is not manager-registered; share context so resolveFile can use 
the files cache.
+    wireFs.setContext(getContext());
+    wireFs.init();
+
+    String prefix = HopWebDavLogicalUris.wirePathPrefixFromRootUrl(rootUrl);
+    return new HopWebDavFileSystem((HopWebDavFileName) rootName, opts, wireFs, 
wireRoot, prefix);
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileSystem.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileSystem.java
new file mode 100644
index 0000000000..8df5d270c6
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileSystem.java
@@ -0,0 +1,141 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.util.Collection;
+import org.apache.commons.vfs2.Capability;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.AbstractFileName;
+import org.apache.commons.vfs2.provider.AbstractFileSystem;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileName;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileProvider;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystem;
+
+class HopWebDavFileSystem extends AbstractFileSystem {
+
+  private final Webdav4FileSystem wireFs;
+  private final Webdav4FileName wireRoot;
+  private final String rootWirePathPrefix;
+
+  HopWebDavFileSystem(
+      HopWebDavFileName rootName,
+      FileSystemOptions fileSystemOptions,
+      Webdav4FileSystem wireFs,
+      Webdav4FileName wireRoot,
+      String rootWirePathPrefix) {
+    super(rootName, null, fileSystemOptions);
+    this.wireFs = wireFs;
+    this.wireRoot = wireRoot;
+    this.rootWirePathPrefix = rootWirePathPrefix;
+  }
+
+  @Override
+  protected FileObject createFile(AbstractFileName name) throws Exception {
+    HopWebDavFileName logical = (HopWebDavFileName) name;
+    FileObject delegate = wireFs.resolveFile(toWire(logical));
+    return new HopWebDavFileObject(logical, this, delegate);
+  }
+
+  @Override
+  protected void addCapabilities(Collection<Capability> caps) {
+    caps.addAll(new Webdav4FileProvider().getCapabilities());
+  }
+
+  /**
+   * Register a logical file object built from a DAV listing so a later {@code 
resolveFile} on the
+   * same name reuses it (and the existing wire delegate) instead of {@link 
#createFile}.
+   */
+  void rememberListedChild(HopWebDavFileObject fo) throws FileSystemException {
+    putFileToCache(fo);
+  }
+
+  Webdav4FileName toWire(HopWebDavFileName logical) throws Exception {
+    if (isLogicalRoot(logical)) {
+      return wireRoot;
+    }
+    String rel = effectiveRelativeSegment(rootWirePathPrefix, 
logical.getPath());
+    String wirePath = wirePathCombine(rootWirePathPrefix, rel);
+    FileName child = wireRoot.createName(wirePath, logical.getType());
+    return HopWebDavWireNames.asWebdav4(child);
+  }
+
+  private static boolean isLogicalRoot(HopWebDavFileName logical) {
+    String p = logical.getPath();
+    return p == null || p.isEmpty() || "/".equals(p);
+  }
+
+  /**
+   * When {@code rootUrl} already ends at e.g. {@code 
/remote.php/dav/files/admin}, a logical URI
+   * that repeats that path ({@code myconn:///remote.php/dav/files/admin/}) 
must not be appended
+   * again under the wire prefix.
+   */
+  static String effectiveRelativeSegment(String rootWirePathPrefix, String 
logicalPath) {
+    String rel = normalizeLogicalPathToRelativeSegment(logicalPath);
+    if (rel.isEmpty()) {
+      return "";
+    }
+    String prefix = rootWirePathPrefix == null ? "/" : rootWirePathPrefix;
+    if (!prefix.startsWith("/")) {
+      prefix = "/" + prefix;
+    }
+    String rootNoTrail = prefix;
+    while (rootNoTrail.length() > 1 && rootNoTrail.endsWith("/")) {
+      rootNoTrail = rootNoTrail.substring(0, rootNoTrail.length() - 1);
+    }
+    if ("/".equals(rootNoTrail)) {
+      return rel;
+    }
+    String logicalAbs = "/" + rel;
+    if (logicalAbs.equals(rootNoTrail)) {
+      return "";
+    }
+    if (logicalAbs.startsWith(rootNoTrail + "/")) {
+      return logicalAbs.substring(rootNoTrail.length() + 1);
+    }
+    return rel;
+  }
+
+  /** Logical path {@code /a/b} → {@code a/b} for appending under DAV root. */
+  private static String normalizeLogicalPathToRelativeSegment(String path) {
+    String p = path == null ? "" : path;
+    if (p.startsWith("/")) {
+      p = p.substring(1);
+    }
+    while (p.length() > 1 && p.endsWith("/")) {
+      p = p.substring(0, p.length() - 1);
+    }
+    return p;
+  }
+
+  static String wirePathCombine(String rootPrefix, String 
relativeNoLeadingSlash) {
+    String prefix = rootPrefix == null ? "/" : rootPrefix;
+    if (!prefix.endsWith("/")) {
+      prefix = prefix + "/";
+    }
+    if (relativeNoLeadingSlash == null || relativeNoLeadingSlash.isEmpty()) {
+      String p = prefix;
+      if (p.endsWith("/") && p.length() > 1) {
+        p = p.substring(0, p.length() - 1);
+      }
+      return p;
+    }
+    return prefix + relativeNoLeadingSlash;
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUris.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUris.java
new file mode 100644
index 0000000000..32045691aa
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUris.java
@@ -0,0 +1,95 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.net.URI;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.hop.core.Const;
+
+/** Helpers for logical {@code <connection-name>:///…} URIs vs internal WebDAV 
wire paths. */
+final class HopWebDavLogicalUris {
+
+  private HopWebDavLogicalUris() {}
+
+  /**
+   * Path prefix under the WebDAV host for {@link
+   * org.apache.hop.vfs.webdav.metadata.WebDavConnection#getRootUrl()}, always 
starting with {@code
+   * /} and ending with {@code /} when non-empty.
+   */
+  static String wirePathPrefixFromRootUrl(String resolvedRootUrl) throws 
FileSystemException {
+    String rootUrl = Const.NVL(resolvedRootUrl, "").trim();
+    if (StringUtils.isEmpty(rootUrl)) {
+      throw new FileSystemException("WebDAV connection has no rootUrl");
+    }
+    URI u;
+    try {
+      u = URI.create(rootUrl);
+    } catch (IllegalArgumentException e) {
+      throw new FileSystemException(e);
+    }
+    String p = u.getPath();
+    if (p == null || p.isEmpty()) {
+      return "/";
+    }
+    if (!p.endsWith("/")) {
+      p = p + "/";
+    }
+    if (!p.startsWith("/")) {
+      p = "/" + p;
+    }
+    return p;
+  }
+
+  /**
+   * Parsed connection URI path for {@link HopWebDavFileNameParser} and for 
VFS {@code resolveName}
+   * output (leading {@code /}, slashes after {@code scheme://} collapsed).
+   *
+   * <p>Does not use {@link URI#create(String)}: WebDAV paths may contain 
spaces and other
+   * characters that are illegal in strict URIs.
+   */
+  static String rawPathFromUri(String uri) throws FileSystemException {
+    if (StringUtils.isEmpty(uri)) {
+      return "";
+    }
+    int colon = uri.indexOf(':');
+    if (colon < 0) {
+      throw new FileSystemException("Missing URI scheme: " + uri);
+    }
+    String rest = uri.substring(colon + 1);
+    int hash = rest.indexOf('#');
+    if (hash >= 0) {
+      rest = rest.substring(0, hash);
+    }
+    if (!rest.startsWith("//")) {
+      throw new FileSystemException("Expected // after scheme in connection 
URI: " + uri);
+    }
+    rest = rest.substring(2);
+    int q = rest.indexOf('?');
+    if (q >= 0) {
+      rest = rest.substring(0, q);
+    }
+    while (rest.startsWith("/")) {
+      rest = rest.substring(1);
+    }
+    String path = rest.isEmpty() ? "" : "/" + rest;
+    while (path.startsWith("//")) {
+      path = path.substring(1);
+    }
+    return path;
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireBootstrap.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireBootstrap.java
new file mode 100644
index 0000000000..1d39507dc3
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireBootstrap.java
@@ -0,0 +1,79 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.UserAuthenticationData;
+import org.apache.commons.vfs2.provider.GenericFileName;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileProvider;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystem;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystemConfigBuilder;
+import org.apache.commons.vfs2.provider.webdav4s.Webdav4sFileProvider;
+import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.protocol.HttpClientContext;
+
+/** Builds an internal {@link Webdav4FileSystem} using stock WebDAV4 HTTP(S) 
client setup. */
+final class HopWebDavWireBootstrap {
+
+  private HopWebDavWireBootstrap() {}
+
+  static Webdav4FileSystem create(FileSystemOptions opts, GenericFileName 
wireRoot)
+      throws FileSystemException {
+    Webdav4FileSystemConfigBuilder builder = 
Webdav4FileSystemConfigBuilder.getInstance();
+    UserAuthenticationData auth = null;
+    try {
+      auth = UserAuthenticatorUtils.authenticate(opts, 
Webdav4FileProvider.AUTHENTICATOR_TYPES);
+      if (wireRoot.getScheme().equalsIgnoreCase("webdav4s")) {
+        return HttpsWire.create(wireRoot, opts, builder, auth);
+      }
+      return HttpWire.create(wireRoot, opts, builder, auth);
+    } finally {
+      UserAuthenticatorUtils.cleanup(auth);
+    }
+  }
+
+  /** Subclass to access {@link Webdav4FileProvider} protected HTTP factory 
methods. */
+  private static final class HttpWire extends Webdav4FileProvider {
+    static Webdav4FileSystem create(
+        GenericFileName wireRoot,
+        FileSystemOptions opts,
+        Webdav4FileSystemConfigBuilder builder,
+        UserAuthenticationData auth)
+        throws FileSystemException {
+      HttpWire p = new HttpWire();
+      HttpClientContext ctx = p.createHttpClientContext(builder, wireRoot, 
opts, auth);
+      HttpClient client = p.createHttpClient(builder, wireRoot, opts);
+      return new HopRawWebDavFileSystem(wireRoot, opts, client, ctx);
+    }
+  }
+
+  private static final class HttpsWire extends Webdav4sFileProvider {
+    static Webdav4FileSystem create(
+        GenericFileName wireRoot,
+        FileSystemOptions opts,
+        Webdav4FileSystemConfigBuilder builder,
+        UserAuthenticationData auth)
+        throws FileSystemException {
+      HttpsWire p = new HttpsWire();
+      HttpClientContext ctx = p.createHttpClientContext(builder, wireRoot, 
opts, auth);
+      HttpClient client = p.createHttpClient(builder, wireRoot, opts);
+      return new HopRawWebDavFileSystem(wireRoot, opts, client, ctx);
+    }
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireNames.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireNames.java
new file mode 100644
index 0000000000..29f72998f5
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireNames.java
@@ -0,0 +1,51 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.provider.GenericURLFileName;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileName;
+
+/** Normalizes {@link GenericURLFileName} from webdav4 parse/create to {@link 
Webdav4FileName}. */
+final class HopWebDavWireNames {
+
+  private HopWebDavWireNames() {}
+
+  static Webdav4FileName asWebdav4(FileName name) throws FileSystemException {
+    if (name instanceof Webdav4FileName) {
+      return (Webdav4FileName) name;
+    }
+    if (name instanceof GenericURLFileName) {
+      GenericURLFileName g = (GenericURLFileName) name;
+      boolean trailing = g.getType() == FileType.FOLDER;
+      return new Webdav4FileName(
+          g.getScheme(),
+          g.getHostName(),
+          g.getPort(),
+          g.getDefaultPort(),
+          g.getUserName(),
+          g.getPassword(),
+          g.getPath(),
+          g.getType(),
+          g.getQueryString(),
+          trailing);
+    }
+    throw new FileSystemException("Unexpected WebDAV wire file name type: " + 
name.getClass());
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavSecureVfsPlugin.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavSecureVfsPlugin.java
new file mode 100644
index 0000000000..91974b6998
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavSecureVfsPlugin.java
@@ -0,0 +1,51 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.util.Collections;
+import java.util.Map;
+import org.apache.commons.vfs2.provider.FileProvider;
+import org.apache.commons.vfs2.provider.webdav4s.Webdav4sFileProvider;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.vfs.plugin.IVfs;
+import org.apache.hop.core.vfs.plugin.VfsPlugin;
+
+/**
+ * Registers the built-in secure WebDAV scheme through the VFS plugin 
mechanism, aligned with other
+ * storage plugins. Named providers are loaded by {@link WebDavVfsPlugin}.
+ */
+@VfsPlugin(
+    type = "webdav4s",
+    typeDescription = "WebDAV HTTPS VFS plugin",
+    classLoaderGroup = "vfs-webdav")
+public class WebDavSecureVfsPlugin implements IVfs {
+
+  @Override
+  public String[] getUrlSchemes() {
+    return new String[] {"webdav4s"};
+  }
+
+  @Override
+  public FileProvider getProvider() {
+    return new Webdav4sFileProvider();
+  }
+
+  @Override
+  public Map<String, FileProvider> getProviders(IVariables variables) {
+    return Collections.emptyMap();
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavVfsPlugin.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavVfsPlugin.java
new file mode 100644
index 0000000000..5f6d5a7017
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavVfsPlugin.java
@@ -0,0 +1,69 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.provider.FileProvider;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileProvider;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.vfs.plugin.IVfs;
+import org.apache.hop.core.vfs.plugin.VfsPlugin;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.util.HopMetadataUtil;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+
+@VfsPlugin(
+    type = "webdav-connection",
+    typeDescription = "WebDAV VFS (named connections)",
+    classLoaderGroup = "vfs-webdav")
+public class WebDavVfsPlugin implements IVfs {
+
+  @Override
+  public String[] getUrlSchemes() {
+    return new String[] {"webdav4"};
+  }
+
+  @Override
+  public FileProvider getProvider() {
+    return new Webdav4FileProvider();
+  }
+
+  @Override
+  public Map<String, FileProvider> getProviders(IVariables variables) {
+    Map<String, FileProvider> providers = new HashMap<>();
+    try {
+      IHopMetadataProvider metadataProvider =
+          HopMetadataUtil.getStandardHopMetadataProvider(variables);
+      List<WebDavConnection> connections =
+          metadataProvider.getSerializer(WebDavConnection.class).loadAll();
+      for (WebDavConnection connection : connections) {
+        String name = connection.getName();
+        if (StringUtils.isEmpty(name)) {
+          continue;
+        }
+        providers.put(name, new HopWebDavFileProvider(variables, connection));
+      }
+    } catch (Exception e) {
+      LogChannel.GENERAL.logError("Unable to load WebDAV VFS providers", e);
+    }
+    return providers;
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnection.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnection.java
new file mode 100644
index 0000000000..0357decc10
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnection.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hop.vfs.webdav.metadata;
+
+import java.io.Serializable;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadata;
+import org.apache.hop.metadata.api.HopMetadataBase;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
+import org.apache.hop.metadata.api.IHopMetadata;
+
+/**
+ * Named WebDAV connection. Use URLs {@code 
<connection-name>:///path/under/root} where {@code
+ * rootUrl} is a full Commons VFS WebDAV root (for example {@code
+ * webdav4://localhost/remote.php/dav/files/admin/}). Credentials are stored 
here and applied via
+ * VFS options, not embedded in the URL.
+ */
+@Getter
+@Setter
+@HopMetadata(
+    key = "WebDavConnectionDefinition",
+    name = "i18n::WebDavConnection.Name",
+    description = "i18n::WebDavConnection.Description",
+    image = "ui/images/authentication.svg",
+    documentationUrl = "/metadata-types/webdav-connection.html",
+    hopMetadataPropertyType = HopMetadataPropertyType.VFS_WEBDAV_CONNECTION)
+public class WebDavConnection extends HopMetadataBase implements Serializable, 
IHopMetadata {
+
+  @HopMetadataProperty private String description;
+
+  /**
+   * Full WebDAV root URL including scheme {@code webdav4://} or {@code 
webdav4s://}, host, optional
+   * port, and path ending at the user's DAV files root (typically with a 
trailing slash).
+   */
+  @HopMetadataProperty private String rootUrl;
+
+  @HopMetadataProperty private String username;
+
+  @HopMetadataProperty(password = true)
+  private String password;
+
+  @HopMetadataProperty private boolean followRedirects;
+
+  @HopMetadataProperty private boolean preemptiveAuth;
+
+  public WebDavConnection() {
+    followRedirects = true;
+    preemptiveAuth = true;
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnectionEditor.java
 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnectionEditor.java
new file mode 100644
index 0000000000..349fa4719f
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnectionEditor.java
@@ -0,0 +1,232 @@
+/*
+ * 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.hop.vfs.webdav.metadata;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.metadata.MetadataEditor;
+import org.apache.hop.ui.core.metadata.MetadataManager;
+import org.apache.hop.ui.core.widget.PasswordTextVar;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+@GuiPlugin(description = "Editor for WebDAV connection metadata")
+public class WebDavConnectionEditor extends MetadataEditor<WebDavConnection> {
+
+  private static final Class<?> PKG = WebDavConnectionEditor.class;
+
+  private Text wName;
+  private Text wDescription;
+  private TextVar wRootUrl;
+  private TextVar wUsername;
+  private PasswordTextVar wPassword;
+  private Button wFollowRedirects;
+  private Button wPreemptiveAuth;
+
+  public WebDavConnectionEditor(
+      HopGui hopGui, MetadataManager<WebDavConnection> manager, 
WebDavConnection metadata) {
+    super(hopGui, manager, metadata);
+  }
+
+  @Override
+  public void createControl(Composite parent) {
+    PropsUi props = PropsUi.getInstance();
+    int middle = props.getMiddlePct();
+    int margin = PropsUi.getMargin() + 2;
+
+    Control lastControl;
+
+    Label wlName = new Label(parent, SWT.RIGHT);
+    PropsUi.setLook(wlName);
+    wlName.setText(BaseMessages.getString(PKG, 
"WebDavConnectionEditor.Name.Label"));
+    FormData fdlName = new FormData();
+    fdlName.top = new FormAttachment(0, margin);
+    fdlName.left = new FormAttachment(0, 0);
+    fdlName.right = new FormAttachment(middle, -margin);
+    wlName.setLayoutData(fdlName);
+    wName = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wName);
+    FormData fdName = new FormData();
+    fdName.top = new FormAttachment(wlName, 0, SWT.CENTER);
+    fdName.left = new FormAttachment(middle, 0);
+    fdName.right = new FormAttachment(95, 0);
+    wName.setLayoutData(fdName);
+    lastControl = wName;
+
+    Label wlDescription = new Label(parent, SWT.RIGHT);
+    PropsUi.setLook(wlDescription);
+    wlDescription.setText(BaseMessages.getString(PKG, 
"WebDavConnectionEditor.Description.Label"));
+    FormData fdlDescription = new FormData();
+    fdlDescription.top = new FormAttachment(lastControl, margin);
+    fdlDescription.left = new FormAttachment(0, 0);
+    fdlDescription.right = new FormAttachment(middle, -margin);
+    wlDescription.setLayoutData(fdlDescription);
+    wDescription = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wDescription);
+    FormData fdDescription = new FormData();
+    fdDescription.top = new FormAttachment(wlDescription, 0, SWT.CENTER);
+    fdDescription.left = new FormAttachment(middle, 0);
+    fdDescription.right = new FormAttachment(95, 0);
+    wDescription.setLayoutData(fdDescription);
+    lastControl = wDescription;
+
+    Label wlRootUrl = new Label(parent, SWT.RIGHT);
+    PropsUi.setLook(wlRootUrl);
+    wlRootUrl.setText(BaseMessages.getString(PKG, 
"WebDavConnectionEditor.RootUrl.Label"));
+    FormData fdlRootUrl = new FormData();
+    fdlRootUrl.top = new FormAttachment(lastControl, margin);
+    fdlRootUrl.left = new FormAttachment(0, 0);
+    fdlRootUrl.right = new FormAttachment(middle, -margin);
+    wlRootUrl.setLayoutData(fdlRootUrl);
+    wRootUrl = new TextVar(getVariables(), parent, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wRootUrl);
+    FormData fdRootUrl = new FormData();
+    fdRootUrl.top = new FormAttachment(wlRootUrl, 0, SWT.CENTER);
+    fdRootUrl.left = new FormAttachment(middle, 0);
+    fdRootUrl.right = new FormAttachment(95, 0);
+    wRootUrl.setLayoutData(fdRootUrl);
+    lastControl = wRootUrl;
+
+    Label wlUsername = new Label(parent, SWT.RIGHT);
+    PropsUi.setLook(wlUsername);
+    wlUsername.setText(BaseMessages.getString(PKG, 
"WebDavConnectionEditor.Username.Label"));
+    FormData fdlUsername = new FormData();
+    fdlUsername.top = new FormAttachment(lastControl, margin);
+    fdlUsername.left = new FormAttachment(0, 0);
+    fdlUsername.right = new FormAttachment(middle, -margin);
+    wlUsername.setLayoutData(fdlUsername);
+    wUsername = new TextVar(getVariables(), parent, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wUsername);
+    FormData fdUsername = new FormData();
+    fdUsername.top = new FormAttachment(wlUsername, 0, SWT.CENTER);
+    fdUsername.left = new FormAttachment(middle, 0);
+    fdUsername.right = new FormAttachment(95, 0);
+    wUsername.setLayoutData(fdUsername);
+    lastControl = wUsername;
+
+    Label wlPassword = new Label(parent, SWT.RIGHT);
+    PropsUi.setLook(wlPassword);
+    wlPassword.setText(BaseMessages.getString(PKG, 
"WebDavConnectionEditor.Password.Label"));
+    FormData fdlPassword = new FormData();
+    fdlPassword.top = new FormAttachment(lastControl, margin);
+    fdlPassword.left = new FormAttachment(0, 0);
+    fdlPassword.right = new FormAttachment(middle, -margin);
+    wlPassword.setLayoutData(fdlPassword);
+    wPassword = new PasswordTextVar(getVariables(), parent, SWT.SINGLE | 
SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wPassword);
+    FormData fdPassword = new FormData();
+    fdPassword.top = new FormAttachment(wlPassword, 0, SWT.CENTER);
+    fdPassword.left = new FormAttachment(middle, 0);
+    fdPassword.right = new FormAttachment(95, 0);
+    wPassword.setLayoutData(fdPassword);
+    lastControl = wPassword;
+
+    Label wlFollowRedirects = new Label(parent, SWT.RIGHT);
+    PropsUi.setLook(wlFollowRedirects);
+    wlFollowRedirects.setText(
+        BaseMessages.getString(PKG, 
"WebDavConnectionEditor.FollowRedirects.Label"));
+    FormData fdlFollowRedirects = new FormData();
+    fdlFollowRedirects.top = new FormAttachment(lastControl, margin);
+    fdlFollowRedirects.left = new FormAttachment(0, 0);
+    fdlFollowRedirects.right = new FormAttachment(middle, -margin);
+    wlFollowRedirects.setLayoutData(fdlFollowRedirects);
+    wFollowRedirects = new Button(parent, SWT.CHECK);
+    PropsUi.setLook(wFollowRedirects);
+    FormData fdFollowRedirects = new FormData();
+    fdFollowRedirects.top = new FormAttachment(wlFollowRedirects, 0, 
SWT.CENTER);
+    fdFollowRedirects.left = new FormAttachment(middle, 0);
+    fdFollowRedirects.right = new FormAttachment(95, 0);
+    wFollowRedirects.setLayoutData(fdFollowRedirects);
+    lastControl = wFollowRedirects;
+
+    Label wlPreemptive = new Label(parent, SWT.RIGHT);
+    PropsUi.setLook(wlPreemptive);
+    wlPreemptive.setText(
+        BaseMessages.getString(PKG, 
"WebDavConnectionEditor.PreemptiveAuth.Label"));
+    FormData fdlPreemptive = new FormData();
+    fdlPreemptive.top = new FormAttachment(lastControl, margin);
+    fdlPreemptive.left = new FormAttachment(0, 0);
+    fdlPreemptive.right = new FormAttachment(middle, -margin);
+    wlPreemptive.setLayoutData(fdlPreemptive);
+    wPreemptiveAuth = new Button(parent, SWT.CHECK);
+    PropsUi.setLook(wPreemptiveAuth);
+    FormData fdPreemptive = new FormData();
+    fdPreemptive.top = new FormAttachment(wlPreemptive, 0, SWT.CENTER);
+    fdPreemptive.left = new FormAttachment(middle, 0);
+    fdPreemptive.right = new FormAttachment(95, 0);
+    wPreemptiveAuth.setLayoutData(fdPreemptive);
+
+    setWidgetsContent();
+
+    wName.addModifyListener(e -> setChanged());
+    wDescription.addModifyListener(e -> setChanged());
+    wRootUrl.addModifyListener(e -> setChanged());
+    wUsername.addModifyListener(e -> setChanged());
+    wPassword.addModifyListener(e -> setChanged());
+    wFollowRedirects.addListener(SWT.Selection, e -> setChanged());
+    wPreemptiveAuth.addListener(SWT.Selection, e -> setChanged());
+  }
+
+  @Override
+  public void setWidgetsContent() {
+    WebDavConnection c = getMetadata();
+    wName.setText(Const.NVL(c.getName(), ""));
+    wDescription.setText(Const.NVL(c.getDescription(), ""));
+    wRootUrl.setText(Const.NVL(c.getRootUrl(), ""));
+    wUsername.setText(Const.NVL(c.getUsername(), ""));
+    wPassword.setText(Const.NVL(c.getPassword(), ""));
+    wFollowRedirects.setSelection(c.isFollowRedirects());
+    wPreemptiveAuth.setSelection(c.isPreemptiveAuth());
+  }
+
+  @Override
+  public void getWidgetsContent(WebDavConnection c) {
+    c.setName(wName.getText());
+    c.setDescription(wDescription.getText());
+    c.setRootUrl(wRootUrl.getText());
+    c.setUsername(wUsername.getText());
+    c.setPassword(wPassword.getText());
+    c.setFollowRedirects(wFollowRedirects.getSelection());
+    c.setPreemptiveAuth(wPreemptiveAuth.getSelection());
+  }
+
+  @Override
+  public boolean setFocus() {
+    if (wName == null || wName.isDisposed()) {
+      return false;
+    }
+    return wName.setFocus();
+  }
+
+  @Override
+  public void save() throws HopException {
+    super.save();
+    HopVfs.reset();
+  }
+}
diff --git 
a/plugins/tech/webdav/src/main/resources/org/apache/hop/vfs/webdav/metadata/messages/messages_en_US.properties
 
b/plugins/tech/webdav/src/main/resources/org/apache/hop/vfs/webdav/metadata/messages/messages_en_US.properties
new file mode 100644
index 0000000000..61f6c171e0
--- /dev/null
+++ 
b/plugins/tech/webdav/src/main/resources/org/apache/hop/vfs/webdav/metadata/messages/messages_en_US.properties
@@ -0,0 +1,26 @@
+#
+# 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.
+#
+WebDavConnection.Name = WebDAV Connection
+WebDavConnection.Description = WebDAV root URL and credentials for named VFS 
URLs (connection name as scheme)
+WebDavConnectionEditor.Name.Label = Name
+WebDavConnectionEditor.Description.Label = Description
+WebDavConnectionEditor.RootUrl.Label = WebDAV root URL
+WebDavConnectionEditor.RootUrl.Tooltip = Full URL including scheme webdav4:// 
or webdav4s://, e.g. webdav4s://localhost/remote.php/dav/files/admin/
+WebDavConnectionEditor.Username.Label = Username
+WebDavConnectionEditor.Password.Label = Password
+WebDavConnectionEditor.FollowRedirects.Label = Follow HTTP redirects
+WebDavConnectionEditor.PreemptiveAuth.Label = Preemptive basic authentication
diff --git a/plugins/tech/webdav/src/main/resources/version.xml 
b/plugins/tech/webdav/src/main/resources/version.xml
new file mode 100644
index 0000000000..36ab20e22e
--- /dev/null
+++ b/plugins/tech/webdav/src/main/resources/version.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+
+<version>${project.version}</version>
diff --git 
a/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParserTest.java
 
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParserTest.java
new file mode 100644
index 0000000000..7ff4f861d3
--- /dev/null
+++ 
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParserTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+import org.junit.jupiter.api.Test;
+
+class HopWebDavFileNameParserTest {
+
+  @Test
+  void parseRoot() throws Exception {
+    WebDavConnection c = new WebDavConnection();
+    c.setName("mywebdav");
+    HopWebDavFileNameParser p = new HopWebDavFileNameParser(c);
+    FileName n = p.parseUri(null, null, "mywebdav:///");
+    assertEquals("mywebdav:///", n.getURI());
+    assertEquals(FileType.FOLDER, n.getType());
+  }
+
+  @Test
+  void parseFile_withSpaceInName() throws Exception {
+    WebDavConnection c = new WebDavConnection();
+    c.setName("myconn");
+    HopWebDavFileNameParser p = new HopWebDavFileNameParser(c);
+    FileName n = p.parseUri(null, null, "myconn:///Documents/New 
document.docx");
+    assertEquals("myconn:///Documents/New document.docx", n.getURI());
+    assertEquals(FileType.FILE, n.getType());
+  }
+
+  @Test
+  void parseFile() throws Exception {
+    WebDavConnection c = new WebDavConnection();
+    c.setName("myconn");
+    HopWebDavFileNameParser p = new HopWebDavFileNameParser(c);
+    FileName n = p.parseUri(null, null, "myconn:///Documents/x.txt");
+    assertEquals("myconn:///Documents/x.txt", n.getURI());
+    assertEquals(FileType.FILE, n.getType());
+  }
+
+  @Test
+  void wrongSchemeThrows() {
+    WebDavConnection c = new WebDavConnection();
+    c.setName("a");
+    HopWebDavFileNameParser p = new HopWebDavFileNameParser(c);
+    assertThrows(FileSystemException.class, () -> p.parseUri(null, null, 
"b:///x"));
+  }
+}
diff --git 
a/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameTest.java
 
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameTest.java
new file mode 100644
index 0000000000..798dbb0534
--- /dev/null
+++ 
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.commons.vfs2.FileType;
+import org.junit.jupiter.api.Test;
+
+class HopWebDavFileNameTest {
+
+  @Test
+  void getUri_usesConnectionScheme() {
+    HopWebDavFileName n = new HopWebDavFileName("mywebdav", 
"/Documents/x.txt", FileType.FILE);
+    assertEquals("mywebdav", n.getScheme());
+    assertEquals("mywebdav:///Documents/x.txt", n.getURI());
+    assertEquals("mywebdav:///", n.getRootURI());
+  }
+
+  @Test
+  void createName_preservesScheme() {
+    HopWebDavFileName parent = new HopWebDavFileName("mywebdav", "/Documents", 
FileType.FOLDER);
+    HopWebDavFileName child =
+        (HopWebDavFileName) parent.createName("/Documents/y.bin", 
FileType.FILE);
+    assertEquals("mywebdav:///Documents/y.bin", child.getURI());
+  }
+
+  @Test
+  void buildChild_underRoot() {
+    HopWebDavFileName parent = new HopWebDavFileName("mywebdav", "/", 
FileType.FOLDER);
+    HopWebDavFileName child = HopWebDavFileName.buildChild(parent, 
"Readme.md", FileType.FILE);
+    assertEquals("mywebdav:///Readme.md", child.getURI());
+  }
+
+  @Test
+  void buildChild_underFolder() {
+    HopWebDavFileName parent = new HopWebDavFileName("mywebdav", 
"/Documents/", FileType.FOLDER);
+    HopWebDavFileName child = HopWebDavFileName.buildChild(parent, "y.bin", 
FileType.FILE);
+    assertEquals("mywebdav:///Documents/y.bin", child.getURI());
+  }
+}
diff --git 
a/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUrisTest.java
 
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUrisTest.java
new file mode 100644
index 0000000000..73a1145269
--- /dev/null
+++ 
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUrisTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class HopWebDavLogicalUrisTest {
+
+  @Test
+  void rawPathFromUri_acceptsSpacesAndExtraSlashesFromVfsResolveName() throws 
Exception {
+    assertEquals(
+        "/remote.php/dav/files/admin/New document.docx",
+        HopWebDavLogicalUris.rawPathFromUri(
+            "mywebdav:////remote.php/dav/files/admin/New document.docx"));
+    assertEquals(
+        "/Documents/New document.docx",
+        HopWebDavLogicalUris.rawPathFromUri("myconn:///Documents/New 
document.docx"));
+  }
+
+  @Test
+  void wirePathPrefixFromRootUrl_normalizesSlash() throws Exception {
+    assertEquals(
+        "/remote.php/dav/files/admin/",
+        HopWebDavLogicalUris.wirePathPrefixFromRootUrl(
+            "webdav4://localhost/remote.php/dav/files/admin"));
+  }
+
+  @Test
+  void wirePathCombine_appendsRelativeUnderPrefix() {
+    assertEquals(
+        "/remote.php/dav/files/admin/Documents/x.txt",
+        HopWebDavFileSystem.wirePathCombine("/remote.php/dav/files/admin/", 
"Documents/x.txt"));
+  }
+
+  @Test
+  void effectiveRelativeSegment_stripsDuplicateOfRootPrefix() {
+    assertEquals(
+        "",
+        HopWebDavFileSystem.effectiveRelativeSegment(
+            "/remote.php/dav/files/admin/", "/remote.php/dav/files/admin"));
+    assertEquals(
+        "",
+        HopWebDavFileSystem.effectiveRelativeSegment(
+            "/remote.php/dav/files/admin/", "/remote.php/dav/files/admin/"));
+  }
+
+  @Test
+  void effectiveRelativeSegment_keepsSuffixBeyondRootPrefix() {
+    assertEquals(
+        "Documents",
+        HopWebDavFileSystem.effectiveRelativeSegment(
+            "/remote.php/dav/files/admin/", 
"/remote.php/dav/files/admin/Documents"));
+    assertEquals(
+        "Documents/x.txt",
+        HopWebDavFileSystem.effectiveRelativeSegment(
+            "/remote.php/dav/files/admin/", 
"/remote.php/dav/files/admin/Documents/x.txt"));
+  }
+
+  @Test
+  void effectiveRelativeSegment_forHostRootPrefix_passesThroughLogicalPath() {
+    assertEquals(
+        "remote.php/dav/files/admin",
+        HopWebDavFileSystem.effectiveRelativeSegment("/", 
"/remote.php/dav/files/admin/"));
+  }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
index a63c66ba2a..7b0f75a6d2 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
@@ -1247,7 +1247,7 @@ public class HopVfsFileDialog implements IFileDialog, 
IDirectoryDialog {
           }
         }
 
-        if (HopVfs.getFileObject(filename).isFolder()) {
+        if (HopVfs.getFileObject(filename, variables).isFolder()) {
           String fullPath = FilenameUtils.concat(filename, saveFilename);
           wFilename.setText(fullPath);
           // Select the saveFilename part...

Reply via email to