aboutsummaryrefslogtreecommitdiff
path: root/fstest/testserver
diff options
context:
space:
mode:
authorMichael Tews <michael@tews.dev>2026-03-06 10:58:19 +0100
committerMichael Tews <michael@tews.dev>2026-03-12 15:23:26 +0100
commitce94f2d69a5f1aab1fc8fc2947f0a6cfd81bb4d1 (patch)
tree4ae72baff8af7a73d46ccd544463aac3f638df39 /fstest/testserver
parenta59763b4ff8c5728401232a696dfc8a725cf4e02 (diff)
test: adds fstest from rclone
Signed-off-by: Michael Tews <michael@tews.dev>
Diffstat (limited to 'fstest/testserver')
-rw-r--r--fstest/testserver/images/test-hdfs/Dockerfile45
-rw-r--r--fstest/testserver/images/test-hdfs/README.md57
-rw-r--r--fstest/testserver/images/test-hdfs/core-site.xml12
-rw-r--r--fstest/testserver/images/test-hdfs/hdfs-site.xml31
-rw-r--r--fstest/testserver/images/test-hdfs/httpfs-site.xml2
-rw-r--r--fstest/testserver/images/test-hdfs/kdc.conf4
-rw-r--r--fstest/testserver/images/test-hdfs/kms-site.xml2
-rw-r--r--fstest/testserver/images/test-hdfs/krb5.conf10
-rw-r--r--fstest/testserver/images/test-hdfs/mapred-site.xml5
-rwxr-xr-xfstest/testserver/images/test-hdfs/run.sh33
-rw-r--r--fstest/testserver/images/test-hdfs/yarn-site.xml14
-rw-r--r--fstest/testserver/images/test-sftp-openssh/Dockerfile11
-rw-r--r--fstest/testserver/images/test-sftp-openssh/README.md17
-rw-r--r--fstest/testserver/init.d/PORTS.md49
-rw-r--r--fstest/testserver/init.d/README.md48
-rwxr-xr-xfstest/testserver/init.d/TestFTPProftpd25
-rwxr-xr-xfstest/testserver/init.d/TestFTPPureftpd29
-rwxr-xr-xfstest/testserver/init.d/TestFTPRclone22
-rwxr-xr-xfstest/testserver/init.d/TestFTPVsftpd26
-rwxr-xr-xfstest/testserver/init.d/TestFTPVsftpdTLS26
-rwxr-xr-xfstest/testserver/init.d/TestHdfs31
-rwxr-xr-xfstest/testserver/init.d/TestS3Exaba30
-rwxr-xr-xfstest/testserver/init.d/TestS3Minio27
-rwxr-xr-xfstest/testserver/init.d/TestS3MinioEdge27
-rwxr-xr-xfstest/testserver/init.d/TestS3Rclone22
-rwxr-xr-xfstest/testserver/init.d/TestSFTPOpenssh26
-rwxr-xr-xfstest/testserver/init.d/TestSFTPRclone22
-rwxr-xr-xfstest/testserver/init.d/TestSFTPRcloneSSH67
-rwxr-xr-xfstest/testserver/init.d/TestSMB33
-rwxr-xr-xfstest/testserver/init.d/TestSMBKerberos84
-rwxr-xr-xfstest/testserver/init.d/TestSMBKerberosCcache85
-rwxr-xr-xfstest/testserver/init.d/TestSeafile72
-rwxr-xr-xfstest/testserver/init.d/TestSeafileEncrypted65
-rwxr-xr-xfstest/testserver/init.d/TestSeafileV648
-rwxr-xr-xfstest/testserver/init.d/TestSia55
-rwxr-xr-xfstest/testserver/init.d/TestSwiftAIO25
-rwxr-xr-xfstest/testserver/init.d/TestSwiftAIO.d/remakerings46
-rwxr-xr-xfstest/testserver/init.d/TestSwiftAIOsegments26
-rwxr-xr-xfstest/testserver/init.d/TestWebdavInfiniteScale46
-rwxr-xr-xfstest/testserver/init.d/TestWebdavNextcloud29
-rwxr-xr-xfstest/testserver/init.d/TestWebdavOwncloud33
-rwxr-xr-xfstest/testserver/init.d/TestWebdavRclone22
-rw-r--r--fstest/testserver/init.d/docker.bash22
-rw-r--r--fstest/testserver/init.d/rclone-serve.bash42
-rw-r--r--fstest/testserver/init.d/run.bash101
-rw-r--r--fstest/testserver/init.d/seafile/docker-compose.yml31
-rw-r--r--fstest/testserver/testserver.go198
47 files changed, 1783 insertions, 0 deletions
diff --git a/fstest/testserver/images/test-hdfs/Dockerfile b/fstest/testserver/images/test-hdfs/Dockerfile
new file mode 100644
index 0000000..07c621d
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/Dockerfile
@@ -0,0 +1,45 @@
+# A very minimal hdfs server for integration testing rclone
+FROM debian:stretch
+
+RUN apt-get update \
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openjdk-8-jdk \
+ net-tools curl python krb5-user krb5-kdc krb5-admin-server \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/
+
+ENV HADOOP_VERSION 3.2.1
+ENV HADOOP_URL https://www.apache.org/dist/hadoop/common/hadoop-$HADOOP_VERSION/hadoop-$HADOOP_VERSION.tar.gz
+RUN set -x \
+ && curl -fSL "$HADOOP_URL" -o /tmp/hadoop.tar.gz \
+ && tar -xvf /tmp/hadoop.tar.gz -C /opt/ \
+ && rm /tmp/hadoop.tar.gz*
+
+RUN ln -s /opt/hadoop-$HADOOP_VERSION/etc/hadoop /etc/hadoop
+RUN mkdir /opt/hadoop-$HADOOP_VERSION/logs
+
+RUN mkdir /hadoop-data
+RUN mkdir -p /hadoop/dfs/name
+RUN mkdir -p /hadoop/dfs/data
+
+ENV HADOOP_HOME=/opt/hadoop-$HADOOP_VERSION
+ENV HADOOP_CONF_DIR=/etc/hadoop
+ENV MULTIHOMED_NETWORK=1
+
+ENV USER=root
+ENV PATH $HADOOP_HOME/bin/:$PATH
+
+ADD core-site.xml /etc/hadoop/core-site.xml
+ADD hdfs-site.xml /etc/hadoop/hdfs-site.xml
+ADD httpfs-site.xml /etc/hadoop/httpfs-site.xml
+ADD kms-site.xml /etc/hadoop/kms-site.xml
+ADD mapred-site.xml /etc/hadoop/mapred-site.xml
+ADD yarn-site.xml /etc/hadoop/yarn-site.xml
+
+ADD krb5.conf /etc/
+ADD kdc.conf /etc/krb5kdc/
+RUN echo '*/admin@KERBEROS.RCLONE *' > /etc/krb5kdc/kadm5.acl
+
+ADD run.sh /run.sh
+RUN chmod a+x /run.sh
+CMD ["/run.sh"]
diff --git a/fstest/testserver/images/test-hdfs/README.md b/fstest/testserver/images/test-hdfs/README.md
new file mode 100644
index 0000000..1f760dd
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/README.md
@@ -0,0 +1,57 @@
+# Test HDFS
+
+This is a docker image for rclone's integration tests which runs an
+hdfs filesystem in a docker image.
+
+## Build
+
+```
+docker build --rm -t rclone/test-hdfs .
+docker push rclone/test-hdfs
+```
+
+# Test
+
+configure remote:
+```
+[TestHdfs]
+type = hdfs
+namenode = 127.0.0.1:8020
+username = root
+```
+
+run tests
+```
+cd backend/hdfs
+GO111MODULE=on go test -v
+```
+
+hdfs logs will be available in `.stdout.log` and `.stderr.log`
+
+# Kerberos
+
+test can be run against kerberos-enabled hdfs
+
+1. configure local krb5.conf
+ ```
+ [libdefaults]
+ default_realm = KERBEROS.RCLONE
+ [realms]
+ KERBEROS.RCLONE = {
+ kdc = localhost
+ }
+ ```
+
+2. enable kerberos in remote configuration
+ ```
+ [TestHdfs]
+ ...
+ service_principal_name = hdfs/localhost
+ data_transfer_protection = privacy
+ ```
+
+3. run test
+ ```
+ cd backend/hdfs
+ KERBEROS=true GO111MODULE=on go test -v
+ ``` \ No newline at end of file
diff --git a/fstest/testserver/images/test-hdfs/core-site.xml b/fstest/testserver/images/test-hdfs/core-site.xml
new file mode 100644
index 0000000..061d48d
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/core-site.xml
@@ -0,0 +1,12 @@
+<configuration>
+ <property><name>fs.defaultFS</name><value>hdfs://localhost:8020</value></property>
+ <property><name>hadoop.http.staticuser.user</name><value>root</value></property>
+ <property><name>hadoop.proxyuser.root.groups</name><value>root,nogroup</value></property>
+ <property><name>hadoop.proxyuser.root.hosts</name><value>*</value></property>
+ <!-- KERBEROS BEGIN -->
+ <property><name>hadoop.security.authentication</name><value>kerberos</value></property>
+ <property><name>hadoop.security.authorization</name><value>true</value></property>
+ <property><name>hadoop.rpc.protection</name><value>integrity</value></property>
+ <property><name>hadoop.user.group.static.mapping.overrides</name><value>user=supergroup</value></property>
+ <!-- KERBEROS END -->
+</configuration>
diff --git a/fstest/testserver/images/test-hdfs/hdfs-site.xml b/fstest/testserver/images/test-hdfs/hdfs-site.xml
new file mode 100644
index 0000000..3f3f3a6
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/hdfs-site.xml
@@ -0,0 +1,31 @@
+<configuration>
+ <property><name>dfs.client.use.datanode.hostname</name><value>true</value></property>
+ <property><name>dfs.datanode.data.dir</name><value>file:///hadoop/dfs/data</value></property>
+ <property><name>dfs.datanode.use.datanode.hostname</name><value>true</value></property>
+ <property><name>dfs.namenode.accesstime.precision</name><value>3600000</value></property>
+ <property><name>dfs.namenode.http-bind-host</name><value>0.0.0.0</value></property>
+ <property><name>dfs.namenode.https-bind-host</name><value>0.0.0.0</value></property>
+ <property><name>dfs.namenode.name.dir</name><value>file:///hadoop/dfs/name</value></property>
+ <property><name>dfs.namenode.rpc-bind-host</name><value>0.0.0.0</value></property>
+ <property><name>dfs.namenode.safemode.extension</name><value>5000</value></property>
+ <property><name>dfs.namenode.servicerpc-bind-host</name><value>0.0.0.0</value></property>
+ <property><name>dfs.replication</name><value>2</value></property>
+ <property><name>nfs.dump.dir</name><value>/tmp</value></property>
+ <!-- KERBEROS BEGIN -->
+ <property><name>ignore.secure.ports.for.testing</name><value>true</value></property>
+ <property><name>dfs.safemode.extension</name><value>0</value></property>
+ <property><name>dfs.block.access.token.enable</name><value>true</value></property>
+
+ <property><name>dfs.encrypt.data.transfer</name><value>true</value></property>
+ <property><name>dfs.encrypt.data.transfer.algorithm</name><value>rc4</value></property>
+ <property><name>dfs.encrypt.data.transfer.cipher.suites</name><value>AES/CTR/NoPadding</value></property>
+
+ <property><name>dfs.namenode.kerberos.principal</name> <value>hdfs/_HOST@KERBEROS.RCLONE</value></property>
+ <property><name>dfs.web.authentication.kerberos.principal</name><value>HTTP/_HOST@KERBEROS.RCLONE</value></property>
+ <property><name>dfs.datanode.kerberos.principal</name> <value>hdfs/_HOST@KERBEROS.RCLONE</value></property>
+
+ <property><name>dfs.namenode.keytab.file</name> <value>/etc/hadoop/kerberos.key</value></property>
+ <property><name>dfs.web.authentication.kerberos.keytab</name><value>/etc/hadoop/kerberos.key</value></property>
+ <property><name>dfs.datanode.keytab.file</name> <value>/etc/hadoop/kerberos.key</value></property>
+ <!-- KERBEROS END -->
+</configuration>
diff --git a/fstest/testserver/images/test-hdfs/httpfs-site.xml b/fstest/testserver/images/test-hdfs/httpfs-site.xml
new file mode 100644
index 0000000..8313843
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/httpfs-site.xml
@@ -0,0 +1,2 @@
+<configuration>
+</configuration>
diff --git a/fstest/testserver/images/test-hdfs/kdc.conf b/fstest/testserver/images/test-hdfs/kdc.conf
new file mode 100644
index 0000000..9eeb0bb
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/kdc.conf
@@ -0,0 +1,4 @@
+[realms]
+ KERBEROS.RCLONE = {
+ acl_file = /etc/krb5kdc/kadm5.acl
+ }
diff --git a/fstest/testserver/images/test-hdfs/kms-site.xml b/fstest/testserver/images/test-hdfs/kms-site.xml
new file mode 100644
index 0000000..8313843
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/kms-site.xml
@@ -0,0 +1,2 @@
+<configuration>
+</configuration>
diff --git a/fstest/testserver/images/test-hdfs/krb5.conf b/fstest/testserver/images/test-hdfs/krb5.conf
new file mode 100644
index 0000000..012950b
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/krb5.conf
@@ -0,0 +1,10 @@
+[libdefaults]
+ default_realm = KERBEROS.RCLONE
+ dns_lookup_realm = false
+ dns_lookup_kdc = false
+ forwardable = true
+ proxiable = true
+[realms]
+ KERBEROS.RCLONE = {
+ kdc = localhost
+ }
diff --git a/fstest/testserver/images/test-hdfs/mapred-site.xml b/fstest/testserver/images/test-hdfs/mapred-site.xml
new file mode 100644
index 0000000..9f70286
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/mapred-site.xml
@@ -0,0 +1,5 @@
+<configuration>
+ <property><name>mapreduce.framework.name</name><value>yarn</value></property>
+ <property><name>yarn.nodemanager.bind-host</name><value>0.0.0.0</value></property>
+</configuration>
+
diff --git a/fstest/testserver/images/test-hdfs/run.sh b/fstest/testserver/images/test-hdfs/run.sh
new file mode 100755
index 0000000..207e11a
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/run.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+KERBEROS=${KERBEROS-"false"}
+
+if [ $KERBEROS = "true" ]; then
+ echo prepare kerberos
+ ADMIN_PASSWORD="kerberos"
+ USER_PASSWORD="user"
+
+ echo -e "$ADMIN_PASSWORD\n$ADMIN_PASSWORD" | kdb5_util -r "KERBEROS.RCLONE" create -s
+ echo -e "$ADMIN_PASSWORD\n$ADMIN_PASSWORD" | kadmin.local -q "addprinc hadoop/admin"
+ echo -e "$USER_PASSWORD\n$USER_PASSWORD" | kadmin.local -q "addprinc user"
+ kadmin.local -q 'addprinc -randkey hdfs/localhost'
+ kadmin.local -q 'addprinc -randkey hdfs/rclone-hdfs'
+ kadmin.local -q 'addprinc -randkey HTTP/localhost'
+ kadmin.local -p hadoop/admin -q "ktadd -k /etc/hadoop/kerberos.key hdfs/localhost hdfs/rclone-hdfs HTTP/localhost"
+ service krb5-kdc restart
+ echo -e "$USER_PASSWORD\n" | kinit user
+ klist
+ echo kerberos ready
+else
+ echo drop kerberos from configuration files
+ sed -i '/KERBEROS BEGIN/,/KERBEROS END/d' /etc/hadoop/core-site.xml
+ sed -i '/KERBEROS BEGIN/,/KERBEROS END/d' /etc/hadoop/hdfs-site.xml
+fi
+
+
+echo format namenode
+hdfs namenode -format test
+
+hdfs namenode &
+hdfs datanode &
+exec sleep infinity
diff --git a/fstest/testserver/images/test-hdfs/yarn-site.xml b/fstest/testserver/images/test-hdfs/yarn-site.xml
new file mode 100644
index 0000000..ade8c7f
--- /dev/null
+++ b/fstest/testserver/images/test-hdfs/yarn-site.xml
@@ -0,0 +1,14 @@
+<configuration>
+ <property><name>yarn.log-aggregation-enable</name><value>true</value></property>
+ <property><name>yarn.log.server.url</name><value>http://localhost:8188/applicationhistory/logs/</value></property>
+ <property><name>yarn.nodemanager.aux-services.mapreduce.shuffle.class</name><value>org.apache.hadoop.mapred.ShuffleHandler</value></property>
+ <property><name>yarn.nodemanager.aux-services</name><value>mapreduce_shuffle</value></property>
+ <property><name>yarn.nodemanager.bind-host</name><value>0.0.0.0</value></property>
+ <property><name>yarn.nodemanager.bind-host</name><value>0.0.0.0</value></property>
+ <property><name>yarn.nodemanager.remote-app-log-dir</name><value>/app-logs</value></property>
+ <property><name>yarn.timeline-service.bind-host</name><value>0.0.0.0</value></property>
+ <property><name>yarn.timeline-service.enabled</name><value>true</value></property>
+ <property><name>yarn.timeline-service.generic-application-history.enabled</name><value>true</value></property>
+ <property><name>yarn.timeline-service.hostname</name><value>historyserver.hadoop</value></property>
+ <property><name>yarn.timeline-service.leveldb-timeline-store.path</name><value>/hadoop/yarn/timeline</value></property>
+</configuration>
diff --git a/fstest/testserver/images/test-sftp-openssh/Dockerfile b/fstest/testserver/images/test-sftp-openssh/Dockerfile
new file mode 100644
index 0000000..f7a8455
--- /dev/null
+++ b/fstest/testserver/images/test-sftp-openssh/Dockerfile
@@ -0,0 +1,11 @@
+# A very minimal sftp server for integration testing rclone
+FROM alpine:latest
+
+# User rclone, password password
+RUN \
+ apk add openssh && \
+ ssh-keygen -A && \
+ adduser -D rclone && \
+ echo "rclone:password" | chpasswd
+
+ENTRYPOINT [ "/usr/sbin/sshd", "-D" ]
diff --git a/fstest/testserver/images/test-sftp-openssh/README.md b/fstest/testserver/images/test-sftp-openssh/README.md
new file mode 100644
index 0000000..2e98b44
--- /dev/null
+++ b/fstest/testserver/images/test-sftp-openssh/README.md
@@ -0,0 +1,17 @@
+# Test SFTP Openssh
+
+This is a docker image for rclone's integration tests which runs an
+openssh server in a docker image.
+
+## Build
+
+```
+docker build --rm -t rclone/test-sftp-openssh .
+docker push rclone/test-sftp-openssh
+```
+
+# Test
+
+```
+rclone lsf -R --sftp-host 172.17.0.2 --sftp-user rclone --sftp-pass $(rclone obscure password) :sftp:
+```
diff --git a/fstest/testserver/init.d/PORTS.md b/fstest/testserver/init.d/PORTS.md
new file mode 100644
index 0000000..fc955a2
--- /dev/null
+++ b/fstest/testserver/init.d/PORTS.md
@@ -0,0 +1,49 @@
+# Ports for tests
+
+All these tests need to run on a different port.
+
+They should be bound to localhost so they are not accessible externally.
+
+| Port | Test |
+|:-----:|:----:|
+| 88 | TestHdfs |
+| 750 | TestHdfs |
+| 8020 | TestHdfs |
+| 8086 | TestSeafileV6 |
+| 8087 | TestSeafile |
+| 8088 | TestSeafileEncrypted |
+| 9866 | TestHdfs |
+| 28620 | TestWebdavRclone |
+| 28621 | TestSFTPRclone |
+| 28622 | TestFTPRclone |
+| 28623 | TestSFTPRcloneSSH |
+| 28624 | TestS3Rclone |
+| 28625 | TestS3Minio |
+| 28626 | TestS3MinioEdge |
+| 28627 | TestSFTPOpenssh |
+| 28628 | TestSwiftAIO |
+| 28629 | TestWebdavNextcloud |
+| 28630 | TestSMB |
+| 28631 | TestFTPProftpd |
+| 28632 | TestSwiftAIOsegments |
+| 28633 | TestSMBKerberos |
+| 28634 | TestSMBKerberos |
+| 28635 | TestS3Exaba |
+| 28636 | TestS3Exaba |
+| 28637 | TestSMBKerberosCcache |
+| 28638 | TestSMBKerberosCcache |
+| 28639 | TestWebdavInfiniteScale |
+| 38081 | TestWebdavOwncloud |
+
+## Non localhost tests
+
+All these use `$(docker_ip)` which means they don't work on macOS or
+Windows. It is proabably possible to make them work with some effort
+but will require port forwarding a range of ports and configuring the
+FTP server to only use that range of ports. The FTP server will likely
+need know it is behind a NAT so it advertises the correct external IP.
+
+- TestFTPProftpd
+- TestFTPPureftpd
+- TestFTPVsftpd
+- TestFTPVsftpdTLS
diff --git a/fstest/testserver/init.d/README.md b/fstest/testserver/init.d/README.md
new file mode 100644
index 0000000..c9acd92
--- /dev/null
+++ b/fstest/testserver/init.d/README.md
@@ -0,0 +1,48 @@
+This directory contains scripts to start and stop servers for testing.
+
+The commands are named after the remotes in use. They are executable
+files with the following parameters:
+
+ start - starts the server if not running
+ stop - stops the server if nothing is using it
+ status - returns non-zero exit code if the server is not running
+ reset - stops the server and resets any reference counts
+
+These will be called automatically by test_all if that remote is
+required.
+
+When start is run it should output config parameters for that remote.
+If a `_connect` parameter is output then that will be used for a
+connection test. For example if `_connect=127.0.0.1:80` then a TCP
+connection will be made to `127.0.0.1:80` and only when that succeeds
+will the test continue.
+
+If in addition to `_connect`, `_connect_delay=5s` is also present then
+after the connection succeeds rclone will wait `5s` before continuing.
+This is for servers that aren't quite ready even though they have
+opened their TCP ports.
+
+## Writing new scripts
+
+A docker based server or an `rclone serve` based server should be easy
+to write. Look at one of the examples.
+
+`run.bash` contains boilerplate to be included in a bash script for
+interpreting the command line parameters. This does reference counting
+to ensure multiple copies of the server aren't running at once.
+Including this is mandatory. It will call your `start()`, `stop()` and
+`status()` functions.
+
+`docker.bash` contains library functions to help with docker
+implementations. It contains implementations of `stop()` and
+`status()` so all you have to do is write a `start()` function.
+
+`rclone-serve.bash` contains functions to help with `rclone serve`
+based implementations. It contains implementations of `stop()` and
+`status()` so all you have to do is write a `start()` function which
+should call the `run()` function provided.
+
+Any external TCP or UDP ports used should be unique as any of the
+servers might be running together. So please create a new line in the
+[PORTS](PORTS.md) file to allocate your server a port. Bind any ports
+to localhost so they aren't accessible externally.
diff --git a/fstest/testserver/init.d/TestFTPProftpd b/fstest/testserver/init.d/TestFTPProftpd
new file mode 100755
index 0000000..029909a
--- /dev/null
+++ b/fstest/testserver/init.d/TestFTPProftpd
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=proftpd
+USER=rclone
+PASS=RaidedBannedPokes5
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "FTP_USERNAME=rclone" \
+ -e "FTP_PASSWORD=$PASS" \
+ hauptmedia/proftpd
+
+ echo type=ftp
+ echo host=$(docker_ip)
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo encoding=Asterisk,Ctl,Dot,Slash
+ echo _connect=$(docker_ip):21
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestFTPPureftpd b/fstest/testserver/init.d/TestFTPPureftpd
new file mode 100755
index 0000000..69c9285
--- /dev/null
+++ b/fstest/testserver/init.d/TestFTPPureftpd
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=pureftpd
+USER=rclone
+PASS=AcridSpiesBooks2
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "FTP_USER_NAME=rclone" \
+ -e "FTP_USER_PASS=$PASS" \
+ -e "FTP_USER_HOME=/data" \
+ -e "FTP_MAX_CLIENTS=50" \
+ -e "FTP_MAX_CONNECTIONS=50" \
+ -e "FTP_PASSIVE_PORTS=30000:40000" \
+ stilliard/pure-ftpd
+
+ echo type=ftp
+ echo host=$(docker_ip)
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo encoding=BackSlash,Ctl,Del,Dot,RightSpace,Slash,SquareBracket
+ echo _connect=$(docker_ip):21
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestFTPRclone b/fstest/testserver/init.d/TestFTPRclone
new file mode 100755
index 0000000..85ad26a
--- /dev/null
+++ b/fstest/testserver/init.d/TestFTPRclone
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=rclone-serve-ftp
+USER=rclone
+PASS=FuddleIdlingJell5
+IP=127.0.0.1
+PORT=28622
+
+start() {
+ run rclone serve ftp --user $USER --pass $PASS --addr ${IP}:${PORT} ${DATADIR}
+
+ echo type=ftp
+ echo host=${IP}
+ echo port=$PORT
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo _connect=${IP}:${PORT}
+}
+
+. $(dirname "$0")/rclone-serve.bash
diff --git a/fstest/testserver/init.d/TestFTPVsftpd b/fstest/testserver/init.d/TestFTPVsftpd
new file mode 100755
index 0000000..d33dcf0
--- /dev/null
+++ b/fstest/testserver/init.d/TestFTPVsftpd
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=vsftpd
+USER=rclone
+PASS=TiffedRestedSian4
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "FTP_USER=rclone" \
+ -e "FTP_PASS=$PASS" \
+ fauria/vsftpd
+
+ echo type=ftp
+ echo host=$(docker_ip)
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo writing_mdtm=true
+ echo encoding=Ctl,LeftPeriod,Slash
+ echo _connect=$(docker_ip):21
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestFTPVsftpdTLS b/fstest/testserver/init.d/TestFTPVsftpdTLS
new file mode 100755
index 0000000..ebcd3b0
--- /dev/null
+++ b/fstest/testserver/init.d/TestFTPVsftpdTLS
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=vsftpdtls
+USER=rclone
+PASS=TiffedRestedSian4
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "FTP_USER=rclone" \
+ -e "FTP_PASS=$PASS" \
+ rclone/vsftpd
+
+ echo type=ftp
+ echo host=$(docker_ip)
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo writing_mdtm=true
+ echo encoding=Ctl,LeftPeriod,Slash
+ echo _connect=$(docker_ip):21
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestHdfs b/fstest/testserver/init.d/TestHdfs
new file mode 100755
index 0000000..26aea1a
--- /dev/null
+++ b/fstest/testserver/init.d/TestHdfs
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=rclone-hdfs
+KERBEROS=${KERBEROS-"false"}
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name "rclone-hdfs" \
+ --hostname "rclone-hdfs" \
+ -e "KERBEROS=$KERBEROS" \
+ -p 127.0.0.1:9866:9866 \
+ -p 127.0.0.1:8020:8020 \
+ -p 127.0.0.1:750:750 \
+ -p 127.0.0.1:88:88 \
+ rclone/test-hdfs
+ sleep 30
+
+ if [ $KERBEROS = "true" ]; then
+ docker cp rclone-hdfs:/tmp/krb5cc_0 /tmp/krb5cc_`id -u`
+ fi
+
+ echo type=hdfs
+ echo namenode=127.0.0.1:8020
+ echo username=root
+ echo _connect=127.0.0.1:8020
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestS3Exaba b/fstest/testserver/init.d/TestS3Exaba
new file mode 100755
index 0000000..c9a7f92
--- /dev/null
+++ b/fstest/testserver/init.d/TestS3Exaba
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=exaba
+USER="Use the webui to find the access_key_id"
+PASS="Use the webui to find the secret_access_key"
+PORT=28635
+WEBUIPORT=28636
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "CLUSTER_NAME=rclone" \
+ -e "CLUSTER_SIZE_GB=20" \
+ -p 127.0.0.1:${PORT}:9000 \
+ -p 127.0.0.1:${WEBUIPORT}:9006 \
+ exaba/exaba
+
+ echo type=s3
+ echo provider=Exaba
+ echo access_key_id=$USER
+ echo secret_access_key=$PASS
+ echo endpoint=http://127.0.0.1:${PORT}/
+ echo webui=http://127.0.0.1:${WEBUIPORT}/
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestS3Minio b/fstest/testserver/init.d/TestS3Minio
new file mode 100755
index 0000000..b4d3dde
--- /dev/null
+++ b/fstest/testserver/init.d/TestS3Minio
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=minio
+USER=rclone
+PASS=AxedBodedGinger7
+PORT=28625
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "MINIO_ACCESS_KEY=$USER" \
+ -e "MINIO_SECRET_KEY=$PASS" \
+ -p 127.0.0.1:${PORT}:9000 \
+ minio/minio server /data
+
+ echo type=s3
+ echo provider=Minio
+ echo access_key_id=$USER
+ echo secret_access_key=$PASS
+ echo endpoint=http://127.0.0.1:${PORT}/
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestS3MinioEdge b/fstest/testserver/init.d/TestS3MinioEdge
new file mode 100755
index 0000000..399ec7f
--- /dev/null
+++ b/fstest/testserver/init.d/TestS3MinioEdge
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=minio-edge
+USER=rclone
+PASS=DeniseOxygenEiffel4
+PORT=28626
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "MINIO_ACCESS_KEY=$USER" \
+ -e "MINIO_SECRET_KEY=$PASS" \
+ -p 127.0.0.1:${PORT}:9000 \
+ minio/minio:edge server /data
+
+ echo type=s3
+ echo provider=Minio
+ echo access_key_id=$USER
+ echo secret_access_key=$PASS
+ echo endpoint=http://127.0.0.1:${PORT}/
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestS3Rclone b/fstest/testserver/init.d/TestS3Rclone
new file mode 100755
index 0000000..c336322
--- /dev/null
+++ b/fstest/testserver/init.d/TestS3Rclone
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=rclone-serve-s3
+ACCESS_KEY_ID=rclone
+SECRET_ACCESS_KEY=JoltRogueVerde5
+IP=127.0.0.1
+PORT=28624
+
+start() {
+ run rclone serve s3 --auth-key ${ACCESS_KEY_ID},${SECRET_ACCESS_KEY} --addr ${IP}:${PORT} ${DATADIR}
+
+ echo type=s3
+ echo provider=Rclone
+ echo endpoint=http://${IP}:${PORT}/
+ echo access_key_id=${ACCESS_KEY_ID}
+ echo secret_access_key=${SECRET_ACCESS_KEY}
+ echo _connect=${IP}:${PORT}
+}
+
+. $(dirname "$0")/rclone-serve.bash
diff --git a/fstest/testserver/init.d/TestSFTPOpenssh b/fstest/testserver/init.d/TestSFTPOpenssh
new file mode 100755
index 0000000..91a9c9a
--- /dev/null
+++ b/fstest/testserver/init.d/TestSFTPOpenssh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=rclone-sftp-openssh
+USER=rclone
+PASS=password
+PORT=28627
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name ${NAME} \
+ -p 127.0.0.1:${PORT}:22 \
+ rclone/test-sftp-openssh
+
+ echo type=sftp
+ echo host=127.0.0.1
+ echo port=$PORT
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo copy_is_hardlink=true
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestSFTPRclone b/fstest/testserver/init.d/TestSFTPRclone
new file mode 100755
index 0000000..f553112
--- /dev/null
+++ b/fstest/testserver/init.d/TestSFTPRclone
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=rclone-serve-sftp
+USER=rclone
+PASS=CranesBallotDorsey5
+IP=127.0.0.1
+PORT=28621
+
+start() {
+ run rclone serve sftp --user $USER --pass $PASS --addr ${IP}:${PORT} ${DATADIR}
+
+ echo type=sftp
+ echo host=${IP}
+ echo port=$PORT
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo _connect=${IP}:${PORT}
+}
+
+. $(dirname "$0")/rclone-serve.bash
diff --git a/fstest/testserver/init.d/TestSFTPRcloneSSH b/fstest/testserver/init.d/TestSFTPRcloneSSH
new file mode 100755
index 0000000..989a5e6
--- /dev/null
+++ b/fstest/testserver/init.d/TestSFTPRcloneSSH
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=rclone-serve-sftp-ssh
+IP=127.0.0.1
+PORT=28623
+PRIVATE_KEY=/tmp/${NAME}.key
+PUBLIC_KEY=/tmp/${NAME}.pub
+
+start() {
+
+cat >${PRIVATE_KEY} <<'#EOF'
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAYEAv7e6d+AbxELvRk7sZipketuqgE4/vVbf/6PMuOd1OSPyOOsAOs41
+tvc4Sk4S+/6ReHW4l1DKy5IH0smOsA1k58kKFkN1NChHU5z0CitAiZwdl7zqvxNJqlMmYi
+GTubhQdMnDrq0AAnhyr9TFrcZmYPcp9tHcpt9VQWeLYR/16tT53WnpgTuMWlgyM58bpCh/
+cDO7gOjSyXHhPPxrU1qdr5g/5T9HFgQfi2CX8vk4pDY+Qw1Lnp1MMpKT4i9xWGMU8oDJG3
+08RrzUi9tz1RTePtbs4xXOy8cXOZaAODDQok4iWvJEpGgJYLjhNuHzZiUDcfc1SkXvONui
+7j5RC/rsQOYB5Sd7ATlF4HymAxZJ3iPu+eYBZi7lwIPeug+42WlVon+D5dOYmrgcPpAZv7
+67Lthv62FMmvc1SHHGPZLS3dWfbZeXayve9+wIkKFEuDN76zYAavjSRm9fBKny6J+noJgp
+bDMVNnTfNA28fsNbsCS6OsBjLbiFjMHxhuYACMaVAAAFgBfF8CkXxfApAAAAB3NzaC1yc2
+EAAAGBAL+3unfgG8RC70ZO7GYqZHrbqoBOP71W3/+jzLjndTkj8jjrADrONbb3OEpOEvv+
+kXh1uJdQysuSB9LJjrANZOfJChZDdTQoR1Oc9AorQImcHZe86r8TSapTJmIhk7m4UHTJw6
+6tAAJ4cq/Uxa3GZmD3KfbR3KbfVUFni2Ef9erU+d1p6YE7jFpYMjOfG6Qof3Azu4Do0slx
+4Tz8a1Nana+YP+U/RxYEH4tgl/L5OKQ2PkMNS56dTDKSk+IvcVhjFPKAyRt9PEa81Ivbc9
+UU3j7W7OMVzsvHFzmWgDgw0KJOIlryRKRoCWC44Tbh82YlA3H3NUpF7zjbou4+UQv67EDm
+AeUnewE5ReB8pgMWSd4j7vnmAWYu5cCD3roPuNlpVaJ/g+XTmJq4HD6QGb++uy7Yb+thTJ
+r3NUhxxj2S0t3Vn22Xl2sr3vfsCJChRLgze+s2AGr40kZvXwSp8uifp6CYKWwzFTZ03zQN
+vH7DW7AkujrAYy24hYzB8YbmAAjGlQAAAAMBAAEAAAGABOxf8oIj1Gdvo5uVQI5oJCuN9l
+uMEX2wpOz87earwPrmVoXabKgtAvTYUjgtDqGb9L75LZGak529a7FXY7gEVlt4UdgLo3pB
+UqleLwCrWJ1UuTfVw3BoXOJjwvNfys4r6sPfrZWtwWJ8d318UhkdOfI+9qKvCu4DT3msP6
+NFenFbtU7p+zKfSRaou2CjohSUKTp63zWbbCbrhNhqnSpfkEnVojp8xdj3QmoJnOi/hqAJ
++0jVH06kzUusVounWoC41pTr1Dlnvy+gWhJcZtHNBXixL6JCM9XGh+z0XFgO8YTiHMdTfz
+Q2wf06TdXzOcM6XwPn3azyKmk7sn2v0s1pxGw8eu8tbmdU46xaLijwipn/B7NMsjk2gnqN
+eptwb/SQmIZZcloQZYLx9PejarAHe2NJ5BSJqOrSHZHYXjiSKj4X55lGdDOVCUCf4lmStQ
+qCS2LiM8Uhfga6f3X5EIBY75kqzmovDnPrqjufnCfYjBzQZ/m/txCbnZ9sTdQfXoVFAAAA
+wQC/5nbU5HzZtg7bA3kfBRUNGUSl8nM2zENY9Rxc8sZiL7iH3s1HAVyz8Frvmc1Wgt6EF+
+WhtmNFklOmdYwq0W5+2qRdUN2P9QL+GKbuyp4AvwRmCFNhgm2GrCWQj/rkZ61vYS3bM8J8
+MNJglvU2FktXvwFODhf6Kv/7fZQnJCf2LTMG6hIKF4LdBOSS/0V5MH2v4xu2U64wqQAQnu
+KzG4sRedsSHBGSknROJ7eGvGPZLh56PRb2gYPItoHcTMHqB6UAAADBAM4Xv6tHQFZtL+ul
+FwVVKhr3EKGY3+RV9IBXvXDkhee4i594Yl67BFUSU4eDb4xuek24znwKn+UFERzp+1X02h
+I1dZRdKtzJWOUQIF0FMPHbaPTuS7viT1OrL/PnG1yXUa+ii0qLExYI9qe463e8w6fNwhaT
+Em2wiDZcxr8SjQfqimyfmDizVlLE/xdgJ56eJ+OyXLjpezKKQif9YcFUW/eHej3YEcok6o
+WDwYpXG8z+VwOnOV4UN/sS8pLkJUdpqwAAAMEA7iTUVH3IvXMno0xYrVdhgNtZZGfQnryt
+pRfG/f5eQ1tjEQwE31mrbBcR278YWlQZwrMWZ0hDaJ3Q/Cp0+JySlm17jsA33lnTRmCHF4
+WolX/LlFtH6jLr9SB8GOsn/8lC6IcvkED0UYYBjXipl6Unh9ZPnpbmJK9SgZWKNTGS1NBw
+xcVWIZTKOrpCD+zWH6KCviuhd3J5vBgkSVxTUzDqA7TmnMnUUxDQAAaU7Eqtt3CJuxsJIs
+vmZ2QrVK8TstC/AAAACm5jd0Bkb2dnZXI=
+-----END OPENSSH PRIVATE KEY-----
+#EOF
+chmod 600 ${PRIVATE_KEY}
+
+cat >${PUBLIC_KEY} <<'#EOF'
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/t7p34BvEQu9GTuxmKmR626qATj+9Vt//o8y453U5I/I46wA6zjW29zhKThL7/pF4dbiXUMrLkgfSyY6wDWTnyQoWQ3U0KEdTnPQKK0CJnB2XvOq/E0mqUyZiIZO5uFB0ycOurQACeHKv1MWtxmZg9yn20dym31VBZ4thH/Xq1PndaemBO4xaWDIznxukKH9wM7uA6NLJceE8/GtTWp2vmD/lP0cWBB+LYJfy+TikNj5DDUuenUwykpPiL3FYYxTygMkbfTxGvNSL23PVFN4+1uzjFc7Lxxc5loA4MNCiTiJa8kSkaAlguOE24fNmJQNx9zVKRe8426LuPlEL+uxA5gHlJ3sBOUXgfKYDFkneI+755gFmLuXAg966D7jZaVWif4Pl05iauBw+kBm/vrsu2G/rYUya9zVIccY9ktLd1Z9tl5drK9737AiQoUS4M3vrNgBq+NJGb18EqfLon6egmClsMxU2dN80Dbx+w1uwJLo6wGMtuIWMwfGG5gAIxpU= user@rclone-serve-test
+#EOF
+chmod 600 ${PUBLIC_KEY}
+
+ run rclone serve sftp --authorized-keys "${PUBLIC_KEY}" --addr ${IP}:${PORT} ${DATADIR}
+
+ echo type=sftp
+ echo ssh=ssh -i ${PRIVATE_KEY} -o StrictHostKeyChecking=no -p ${PORT} user@${IP}
+ echo _connect=${IP}:${PORT}
+}
+
+. $(dirname "$0")/rclone-serve.bash
diff --git a/fstest/testserver/init.d/TestSMB b/fstest/testserver/init.d/TestSMB
new file mode 100755
index 0000000..4e10d59
--- /dev/null
+++ b/fstest/testserver/init.d/TestSMB
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=smb
+USER=rclone
+PASS=GNF3Cqeu
+WORKGROUP=thepub
+PORT=28630
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -p 127.0.0.1:${PORT}:445 \
+ -p 127.0.0.1:${PORT}:445/udp \
+ dperson/samba \
+ -p \
+ -u "rclone;${PASS}" \
+ -w "${WORKGROUP}" \
+ -s "public;/share" \
+ -s "rclone;/rclone;yes;no;no;rclone"
+
+ echo type=smb
+ echo host=127.0.0.1
+ echo user=$USER
+ echo port=$PORT
+ echo pass=$(rclone obscure $PASS)
+ echo domain=$WORKGROUP
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestSMBKerberos b/fstest/testserver/init.d/TestSMBKerberos
new file mode 100755
index 0000000..f54eeb6
--- /dev/null
+++ b/fstest/testserver/init.d/TestSMBKerberos
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+
+set -e
+
+IMAGE=rclone/test-smb-kerberos
+NAME=smb-kerberos
+USER=rclone
+DOMAIN=RCLONE
+REALM=RCLONE.LOCAL
+SMB_PORT=28633
+KRB5_PORT=28634
+
+# KRB5_CONFIG and KRB5CCNAME should be set by the caller but default
+# them here for the integration tests
+export TEMP_DIR=/tmp/rclone_krb5
+mkdir -p "${TEMP_DIR}"
+export KRB5_CONFIG=${KRB5_CONFIG:-${TEMP_DIR}/krb5.conf}
+export KRB5CCNAME=${KRB5CCNAME:-${TEMP_DIR}/ccache}
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker build -t ${IMAGE} --load - <<EOF
+FROM alpine:3.21
+RUN apk add --no-cache samba-dc
+RUN rm -rf /etc/samba/smb.conf /var/lib/samba \
+ && mkdir -p /var/lib/samba/private \
+ && samba-tool domain provision \
+ --use-rfc2307 \
+ --option acl_xattr:security_acl_name=user.NTACL \
+ --realm=$REALM \
+ --domain=$DOMAIN \
+ --server-role=dc \
+ --dns-backend=SAMBA_INTERNAL \
+ --host-name=localhost \
+ && samba-tool user add --random-password $USER \
+ && samba-tool user setexpiry $USER --noexpiry \
+ && mkdir -m 777 /share /rclone \
+ && cat <<EOS >> /etc/samba/smb.conf
+[global]
+server signing = auto
+[public]
+path = /share
+browseable = yes
+read only = yes
+guest ok = yes
+[rclone]
+path = /rclone
+browseable = yes
+read only = no
+guest ok = no
+valid users = rclone
+EOS
+CMD ["samba", "-i"]
+EOF
+
+ docker run --rm -d --name ${NAME} \
+ -p 127.0.0.1:${SMB_PORT}:445 \
+ -p 127.0.0.1:${SMB_PORT}:445/udp \
+ -p 127.0.0.1:${KRB5_PORT}:88 \
+ ${IMAGE}
+
+ # KRB5_CONFIG and KRB5CCNAME are set by the caller
+ cat > ${KRB5_CONFIG} <<EOF
+[libdefaults]
+ default_realm = ${REALM}
+[realms]
+${REALM} = {
+ kdc = localhost
+}
+EOF
+ docker cp ${KRB5_CONFIG} ${NAME}:/etc/krb5.conf
+ docker exec ${NAME} samba-tool user get-kerberos-ticket rclone --output-krb5-ccache=/tmp/ccache
+ docker cp ${NAME}:/tmp/ccache ${KRB5CCNAME}
+ sed -i -e "s/localhost/localhost:${KRB5_PORT}/" ${KRB5_CONFIG}
+
+ echo type=smb
+ echo host=localhost
+ echo port=$SMB_PORT
+ echo use_kerberos=true
+ echo _connect=127.0.0.1:${SMB_PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestSMBKerberosCcache b/fstest/testserver/init.d/TestSMBKerberosCcache
new file mode 100755
index 0000000..1238299
--- /dev/null
+++ b/fstest/testserver/init.d/TestSMBKerberosCcache
@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+
+set -e
+
+# Set default location for Kerberos config and ccache. Can be overridden by the caller
+# using environment variables RCLONE_TEST_CUSTOM_CCACHE_LOCATION and KRB5_CONFIG.
+export TEMP_DIR=/tmp/rclone_krb5_ccache
+mkdir -p "${TEMP_DIR}"
+export KRB5_CONFIG=${KRB5_CONFIG:-${TEMP_DIR}/krb5.conf}
+export RCLONE_TEST_CUSTOM_CCACHE_LOCATION=${RCLONE_TEST_CUSTOM_CCACHE_LOCATION:-${TEMP_DIR}/ccache}
+
+IMAGE=rclone/test-smb-kerberos-ccache
+NAME=smb-kerberos-ccache
+USER=rclone
+DOMAIN=RCLONE
+REALM=RCLONE.LOCAL
+SMB_PORT=28637
+KRB5_PORT=28638
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker build -t ${IMAGE} --load - <<EOF
+FROM alpine:3.21
+RUN apk add --no-cache samba-dc
+RUN rm -rf /etc/samba/smb.conf /var/lib/samba \
+ && mkdir -p /var/lib/samba/private \
+ && samba-tool domain provision \
+ --use-rfc2307 \
+ --option acl_xattr:security_acl_name=user.NTACL \
+ --realm=$REALM \
+ --domain=$DOMAIN \
+ --server-role=dc \
+ --dns-backend=SAMBA_INTERNAL \
+ --host-name=localhost \
+ && samba-tool user add --random-password $USER \
+ && samba-tool user setexpiry $USER --noexpiry \
+ && mkdir -m 777 /share /rclone \
+ && cat <<EOS >> /etc/samba/smb.conf
+[global]
+server signing = auto
+[public]
+path = /share
+browseable = yes
+read only = yes
+guest ok = yes
+[rclone]
+path = /rclone
+browseable = yes
+read only = no
+guest ok = no
+valid users = rclone
+EOS
+CMD ["samba", "-i"]
+EOF
+
+ docker run --rm -d --name ${NAME} \
+ -p 127.0.0.1:${SMB_PORT}:445 \
+ -p 127.0.0.1:${SMB_PORT}:445/udp \
+ -p 127.0.0.1:${KRB5_PORT}:88 \
+ ${IMAGE}
+
+ cat > "${KRB5_CONFIG}" <<EOF
+[libdefaults]
+ default_realm = ${REALM}
+[realms]
+${REALM} = {
+ kdc = localhost
+}
+EOF
+
+ docker cp "${KRB5_CONFIG}" ${NAME}:/etc/krb5.conf
+ docker exec ${NAME} samba-tool user get-kerberos-ticket rclone --output-krb5-ccache=/tmp/ccache
+ docker cp ${NAME}:/tmp/ccache "${RCLONE_TEST_CUSTOM_CCACHE_LOCATION}"
+ sed -i -e "s/localhost/localhost:${KRB5_PORT}/" "${KRB5_CONFIG}"
+
+ echo type=smb
+ echo host=localhost
+ echo port=$SMB_PORT
+ echo use_kerberos=true
+ echo kerberos_ccache=${RCLONE_TEST_CUSTOM_CCACHE_LOCATION}
+ echo _connect=127.0.0.1:${SMB_PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestSeafile b/fstest/testserver/init.d/TestSeafile
new file mode 100755
index 0000000..553e9d0
--- /dev/null
+++ b/fstest/testserver/init.d/TestSeafile
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+
+set -e
+
+# environment variables passed on docker-compose
+export NAME=seafile7
+export MYSQL_ROOT_PASSWORD=pixenij4zacoguq0kopamid6
+export SEAFILE_ADMIN_EMAIL=seafile@rclone.org
+export SEAFILE_ADMIN_PASSWORD=pixenij4zacoguq0kopamid6
+export SEAFILE_IP=127.0.0.1
+export SEAFILE_PORT=8087
+export SEAFILE_TEST_DATA=${SEAFILE_TEST_DATA:-/tmp/seafile-test-data}
+export SEAFILE_VERSION=latest
+
+# make sure the data directory exists
+mkdir -p ${SEAFILE_TEST_DATA}/${NAME}
+
+# docker-compose project directory
+COMPOSE_DIR=$(dirname "$0")/seafile
+
+start() {
+ docker-compose --project-directory ${COMPOSE_DIR} --project-name ${NAME} --file ${COMPOSE_DIR}/docker-compose.yml up -d
+
+ # wait for Seafile server to start
+ seafile_endpoint="http://${SEAFILE_IP}:${SEAFILE_PORT}/"
+ wait_seconds=1
+ echo -n "Waiting for Seafile server to start"
+ for iterations in `seq 1 60`;
+ do
+ http_code=$(curl -s -o /dev/null -L -w '%{http_code}' "$seafile_endpoint" || true;)
+ if [ "$http_code" -eq 200 ]; then
+ echo
+ break
+ fi
+ echo -n "."
+ sleep $wait_seconds
+ done
+
+ # authentication token answer should be like: {"token":"dbf58423f1632b5b679a13b0929f1d0751d9250c"}
+ TOKEN=`curl --silent \
+ --data-urlencode username=${SEAFILE_ADMIN_EMAIL} -d password=${SEAFILE_ADMIN_PASSWORD} \
+ http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/auth-token/ \
+ | sed 's/^{"token":"\(.*\)"}$/\1/'`
+
+ # create default library
+ curl --silent -o /dev/null -X POST -H "Authorization: Token ${TOKEN}" "http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/default-repo/"
+
+ echo _connect=${SEAFILE_IP}:${SEAFILE_PORT}
+ echo type=seafile
+ echo url=http://${SEAFILE_IP}:${SEAFILE_PORT}/
+ echo user=${SEAFILE_ADMIN_EMAIL}
+ echo pass=$(rclone obscure ${SEAFILE_ADMIN_PASSWORD})
+ echo library=My Library
+}
+
+stop() {
+ if status ; then
+ docker-compose --project-directory ${COMPOSE_DIR} --project-name ${NAME} --file ${COMPOSE_DIR}/docker-compose.yml down
+ fi
+}
+
+status() {
+ if docker ps --format "{{.Names}}" | grep ^${NAME}_seafile_1$ >/dev/null ; then
+ echo "$NAME running"
+ else
+ echo "$NAME not running"
+ return 1
+ fi
+ return 0
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestSeafileEncrypted b/fstest/testserver/init.d/TestSeafileEncrypted
new file mode 100755
index 0000000..dd4b6bc
--- /dev/null
+++ b/fstest/testserver/init.d/TestSeafileEncrypted
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+
+set -e
+
+# local variables
+TEST_LIBRARY=Encrypted
+TEST_LIBRARY_PASSWORD=SecretKey
+
+# environment variables passed on docker-compose
+export NAME=seafile7encrypted
+export MYSQL_ROOT_PASSWORD=pixenij4zacoguq0kopamid6
+export SEAFILE_ADMIN_EMAIL=seafile@rclone.org
+export SEAFILE_ADMIN_PASSWORD=pixenij4zacoguq0kopamid6
+export SEAFILE_IP=127.0.0.1
+export SEAFILE_PORT=8088
+export SEAFILE_TEST_DATA=${SEAFILE_TEST_DATA:-/tmp/seafile-test-data}
+export SEAFILE_VERSION=latest
+
+# make sure the data directory exists
+mkdir -p ${SEAFILE_TEST_DATA}/${NAME}
+
+# docker-compose project directory
+COMPOSE_DIR=$(dirname "$0")/seafile
+
+start() {
+ docker-compose --project-directory ${COMPOSE_DIR} --project-name ${NAME} --file ${COMPOSE_DIR}/docker-compose.yml up -d
+
+ # it takes some time for the database to be created
+ sleep 60
+
+ # authentication token answer should be like: {"token":"dbf58423f1632b5b679a13b0929f1d0751d9250c"}
+ TOKEN=`curl --silent \
+ --data-urlencode username=${SEAFILE_ADMIN_EMAIL} -d password=${SEAFILE_ADMIN_PASSWORD} \
+ http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/auth-token/ \
+ | sed 's/^{"token":"\(.*\)"}$/\1/'`
+
+ # create encrypted library
+ curl --silent -o /dev/null -X POST -d "name=${TEST_LIBRARY}&passwd=${TEST_LIBRARY_PASSWORD}" -H "Authorization: Token ${TOKEN}" "http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/repos/"
+
+ echo _connect=${SEAFILE_IP}:${SEAFILE_PORT}
+ echo type=seafile
+ echo url=http://${SEAFILE_IP}:${SEAFILE_PORT}/
+ echo user=${SEAFILE_ADMIN_EMAIL}
+ echo pass=$(rclone obscure ${SEAFILE_ADMIN_PASSWORD})
+ echo library=${TEST_LIBRARY}
+ echo library_key=$(rclone obscure ${TEST_LIBRARY_PASSWORD})
+}
+
+stop() {
+ if status ; then
+ docker-compose --project-directory ${COMPOSE_DIR} --project-name ${NAME} --file ${COMPOSE_DIR}/docker-compose.yml down
+ fi
+}
+
+status() {
+ if docker ps --format "{{.Names}}" | grep ^${NAME}_seafile_1$ >/dev/null ; then
+ echo "$NAME running"
+ else
+ echo "$NAME not running"
+ return 1
+ fi
+ return 0
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestSeafileV6 b/fstest/testserver/init.d/TestSeafileV6
new file mode 100755
index 0000000..e2365aa
--- /dev/null
+++ b/fstest/testserver/init.d/TestSeafileV6
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+set -e
+
+# local variables
+NAME=seafile6
+SEAFILE_IP=127.0.0.1
+SEAFILE_PORT=8086
+SEAFILE_ADMIN_EMAIL=seafile@rclone.org
+SEAFILE_ADMIN_PASSWORD=qebiwob7wafixif8sojiboj4
+SEAFILE_TEST_DATA=${SEAFILE_TEST_DATA:-/tmp/seafile-test-data}
+SEAFILE_VERSION=latest
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ # make sure the data directory exists
+ mkdir -p ${SEAFILE_TEST_DATA}/${NAME}
+
+ docker run --rm -d --name $NAME \
+ -e SEAFILE_SERVER_HOSTNAME=${SEAFILE_IP}:${SEAFILE_PORT} \
+ -e SEAFILE_ADMIN_EMAIL=${SEAFILE_ADMIN_EMAIL} \
+ -e SEAFILE_ADMIN_PASSWORD=${SEAFILE_ADMIN_PASSWORD} \
+ -v ${SEAFILE_TEST_DATA}/${NAME}:/shared \
+ -p ${SEAFILE_IP}:${SEAFILE_PORT}:80 \
+ seafileltd/seafile:${SEAFILE_VERSION}
+
+ # it takes some time for the database to be created
+ sleep 60
+
+ # authentication token answer should be like: {"token":"dbf58423f1632b5b679a13b0929f1d0751d9250c"}
+ TOKEN=`curl --silent \
+ --data-urlencode username=${SEAFILE_ADMIN_EMAIL} -d password=${SEAFILE_ADMIN_PASSWORD} \
+ http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/auth-token/ \
+ | sed 's/^{"token":"\(.*\)"}$/\1/'`
+
+ # create default library
+ curl --silent -o /dev/null -X POST -H "Authorization: Token ${TOKEN}" "http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/default-repo/"
+
+ echo _connect=${SEAFILE_IP}:${SEAFILE_PORT}
+ echo type=seafile
+ echo url=http://${SEAFILE_IP}:${SEAFILE_PORT}/
+ echo user=${SEAFILE_ADMIN_EMAIL}
+ echo pass=$(rclone obscure ${SEAFILE_ADMIN_PASSWORD})
+ echo library=My Library
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestSia b/fstest/testserver/init.d/TestSia
new file mode 100755
index 0000000..9b11caf
--- /dev/null
+++ b/fstest/testserver/init.d/TestSia
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=Sia
+
+# shellcheck disable=SC1090
+. "$(dirname "$0")"/docker.bash
+
+# wait until Sia test network is up,
+# the Sia renter forms contracts on the blockchain
+# and the renter is upload ready
+wait_for_sia() {
+ until curl -A Sia-Agent -s "$1" | grep -q '"ready":true'
+ do
+ sleep 5
+ done
+}
+export -f wait_for_sia
+
+start() {
+ # use non-production sia port in test
+ SIA_CONN="127.0.0.1:39980"
+ # nebulouslabs/siaantfarm is stale, use up-to-date image
+ ANTFARM_IMAGE=ivandeex/sia-antfarm:latest
+
+ # pull latest antfarm image (dont use local image)
+ docker pull --quiet $ANTFARM_IMAGE
+
+ # start latest antfarm with default config
+ docker run --rm --detach --name "$NAME" \
+ --publish "${SIA_CONN}:9980" \
+ $ANTFARM_IMAGE
+
+ # wait until the test network is upload ready
+ timeout 300 bash -c "wait_for_sia ${SIA_CONN}/renter/uploadready"
+
+ # confirm backend type in the generated rclone.conf
+ echo "type=sia"
+ # override keys in the Sia section of generated rclone.conf
+ echo "api_url=http://${SIA_CONN}/"
+ # hint test harness where to probe for connection
+ echo "_connect=${SIA_CONN}"
+}
+
+stop() {
+ if status ; then
+ docker logs "$NAME" >> sia-test.log 2>&1
+ docker kill "$NAME"
+ echo "${NAME} stopped"
+ fi
+}
+
+# shellcheck disable=SC1090
+. "$(dirname "$0")"/run.bash
diff --git a/fstest/testserver/init.d/TestSwiftAIO b/fstest/testserver/init.d/TestSwiftAIO
new file mode 100755
index 0000000..7e20bd6
--- /dev/null
+++ b/fstest/testserver/init.d/TestSwiftAIO
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=swift-aio
+PORT=28628
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ # We need to replace the remakerings in the container to create Policy-1.
+ docker run --rm -d --name ${NAME} \
+ -p 127.0.0.1:${PORT}:8080 \
+ -v $(dirname "$0")/TestSwiftAIO.d/remakerings:/etc/swift/remakerings:ro \
+ openstackswift/saio
+
+ echo type=swift
+ echo env_auth=false
+ echo user=test:tester
+ echo key=testing
+ echo auth=http://127.0.0.1:${PORT}/auth/v1.0
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestSwiftAIO.d/remakerings b/fstest/testserver/init.d/TestSwiftAIO.d/remakerings
new file mode 100755
index 0000000..27c49b1
--- /dev/null
+++ b/fstest/testserver/init.d/TestSwiftAIO.d/remakerings
@@ -0,0 +1,46 @@
+#!/bin/sh
+
+if ! grep -q "^\[storage-policy:1\]" swift.conf; then
+ cat <<EOF >> swift.conf
+
+[storage-policy:1]
+name = Policy-1
+EOF
+fi
+
+rm -f *.builder *.ring.gz backups/*.builder backups/*.ring.gz
+
+swift-ring-builder object.builder create 10 1 1
+swift-ring-builder object.builder add r1z1-127.0.0.1:6200/swift-d0 1
+swift-ring-builder object.builder add r1z1-127.0.0.1:6200/swift-d1 1
+swift-ring-builder object.builder add r1z1-127.0.0.1:6200/swift-d2 1
+swift-ring-builder object.builder add r1z1-127.0.0.1:6200/swift-d3 1
+swift-ring-builder object.builder add r1z1-127.0.0.1:6200/swift-d4 1
+swift-ring-builder object.builder add r1z1-127.0.0.1:6200/swift-d5 1
+swift-ring-builder object.builder rebalance
+swift-ring-builder container.builder create 10 1 1
+swift-ring-builder container.builder add r1z1-127.0.0.1:6201/swift-d0 1
+swift-ring-builder container.builder add r1z1-127.0.0.1:6201/swift-d1 1
+swift-ring-builder container.builder add r1z1-127.0.0.1:6201/swift-d2 1
+swift-ring-builder container.builder add r1z1-127.0.0.1:6201/swift-d3 1
+swift-ring-builder container.builder add r1z1-127.0.0.1:6201/swift-d4 1
+swift-ring-builder container.builder add r1z1-127.0.0.1:6201/swift-d5 1
+swift-ring-builder container.builder rebalance
+swift-ring-builder account.builder create 10 1 1
+swift-ring-builder account.builder add r1z1-127.0.0.1:6202/swift-d0 1
+swift-ring-builder account.builder add r1z1-127.0.0.1:6202/swift-d1 1
+swift-ring-builder account.builder add r1z1-127.0.0.1:6202/swift-d2 1
+swift-ring-builder account.builder add r1z1-127.0.0.1:6202/swift-d3 1
+swift-ring-builder account.builder add r1z1-127.0.0.1:6202/swift-d4 1
+swift-ring-builder account.builder add r1z1-127.0.0.1:6202/swift-d5 1
+swift-ring-builder account.builder rebalance
+
+# For Policy-1:
+swift-ring-builder object-1.builder create 10 1 1
+swift-ring-builder object-1.builder add r1z1-127.0.0.1:6200/swift-d0 1
+swift-ring-builder object-1.builder add r1z1-127.0.0.1:6200/swift-d1 1
+swift-ring-builder object-1.builder add r1z1-127.0.0.1:6200/swift-d2 1
+swift-ring-builder object-1.builder add r1z1-127.0.0.1:6200/swift-d3 1
+swift-ring-builder object-1.builder add r1z1-127.0.0.1:6200/swift-d4 1
+swift-ring-builder object-1.builder add r1z1-127.0.0.1:6200/swift-d5 1
+swift-ring-builder object-1.builder rebalance
diff --git a/fstest/testserver/init.d/TestSwiftAIOsegments b/fstest/testserver/init.d/TestSwiftAIOsegments
new file mode 100755
index 0000000..db02630
--- /dev/null
+++ b/fstest/testserver/init.d/TestSwiftAIOsegments
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+set -e
+
+NAME=swift-aio-segments
+PORT=28632
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ # We need to replace the remakerings in the container to create Policy-1.
+ docker run --rm -d --name ${NAME} \
+ -p 127.0.0.1:${PORT}:8080 \
+ -v $(dirname "$0")/TestSwiftAIO.d/remakerings:/etc/swift/remakerings:ro \
+ openstackswift/saio
+
+ echo type=swift
+ echo env_auth=false
+ echo user=test:tester
+ echo key=testing
+ echo auth=http://127.0.0.1:${PORT}/auth/v1.0
+ echo use_segments_container=false
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestWebdavInfiniteScale b/fstest/testserver/init.d/TestWebdavInfiniteScale
new file mode 100755
index 0000000..f13e22b
--- /dev/null
+++ b/fstest/testserver/init.d/TestWebdavInfiniteScale
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=infinitescale
+USER=admin
+PASS=admin
+PORT=28639
+CONF_DIR=/tmp/ocis-config
+mkdir -p ${CONF_DIR}
+chmod 777 ${CONF_DIR} || true
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm --name $NAME \
+ -v ${CONF_DIR}:/etc/ocis \
+ -e "OCIS_INSECURE=true" \
+ -e "IDM_ADMIN_PASSWORD=$PASS" \
+ -e "OCIS_FORCE_CONFIG_OVERWRITE=true" \
+ -e "OCIS_URL=https://127.0.0.1:$PORT" \
+ owncloud/ocis \
+ init
+
+ docker run --rm -d --name $NAME \
+ -e "OCIS_LOG_LEVEL=debug" \
+ -e "OCIS_LOG_PRETTY=true" \
+ -e "OCIS_URL=https://127.0.0.1:$PORT" \
+ -e "OCIS_ADMIN_USER_ID=some-admin-user-id-0000-100000000000" \
+ -e "IDM_ADMIN_PASSWORD=$PASS" \
+ -e "OCIS_INSECURE=true" \
+ -e "PROXY_ENABLE_BASIC_AUTH=true" \
+ -v ${CONF_DIR}:/etc/ocis \
+ -p 127.0.0.1:${PORT}:9200 \
+ owncloud/ocis
+
+ echo type=webdav
+ echo url=https://127.0.0.1:${PORT}/dav/spaces/some-admin-user-id-0000-100000000000
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo vendor=infinitescale
+ echo _connect=127.0.0.1:${PORT}
+ echo _connect_delay=5s
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestWebdavNextcloud b/fstest/testserver/init.d/TestWebdavNextcloud
new file mode 100755
index 0000000..42766e5
--- /dev/null
+++ b/fstest/testserver/init.d/TestWebdavNextcloud
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=nextcloud
+USER=rclone
+PASS=ArmorAbleMale6
+PORT=28629
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "SQLITE_DATABASE=nextcloud.db" \
+ -e "NEXTCLOUD_ADMIN_USER=rclone" \
+ -e "NEXTCLOUD_ADMIN_PASSWORD=$PASS" \
+ -e "NEXTCLOUD_TRUSTED_DOMAINS=*.*.*.*" \
+ -p 127.0.0.1:${PORT}:80 \
+ nextcloud:latest
+
+ echo type=webdav
+ echo url=http://127.0.0.1:${PORT}/remote.php/dav/files/$USER/
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo vendor=nextcloud
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestWebdavOwncloud b/fstest/testserver/init.d/TestWebdavOwncloud
new file mode 100755
index 0000000..d2dc238
--- /dev/null
+++ b/fstest/testserver/init.d/TestWebdavOwncloud
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=owncloud
+USER=rclone
+PASS=HarperGrayerFewest5
+PORT=38081
+
+. $(dirname "$0")/docker.bash
+
+start() {
+ docker run --rm -d --name $NAME \
+ -e "OWNCLOUD_DOMAIN=localhost:8080" \
+ -e "OWNCLOUD_DB_TYPE=sqlite" \
+ -e "OWNCLOUD_DB_NAME=owncloud.db" \
+ -e "OWNCLOUD_ADMIN_USERNAME=$USER" \
+ -e "OWNCLOUD_ADMIN_PASSWORD=$PASS" \
+ -e "OWNCLOUD_MYSQL_UTF8MB4=true" \
+ -e "OWNCLOUD_REDIS_ENABLED=false" \
+ -e "OWNCLOUD_TRUSTED_DOMAINS=127.0.0.1" \
+ -p 127.0.0.1:${PORT}:8080 \
+ owncloud/server
+
+ echo type=webdav
+ echo url=http://127.0.0.1:${PORT}/remote.php/webdav/
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo vendor=owncloud
+ echo _connect=127.0.0.1:${PORT}
+}
+
+. $(dirname "$0")/run.bash
diff --git a/fstest/testserver/init.d/TestWebdavRclone b/fstest/testserver/init.d/TestWebdavRclone
new file mode 100755
index 0000000..e740ecc
--- /dev/null
+++ b/fstest/testserver/init.d/TestWebdavRclone
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -e
+
+NAME=rclone-serve-webdav
+USER=rclone
+PASS=PagansSwimExpiry9
+IP=127.0.0.1
+PORT=28620
+
+start() {
+ run rclone serve webdav --user $USER --pass $PASS --addr ${IP}:${PORT} ${DATADIR}
+
+ echo type=webdav
+ echo vendor=rclone
+ echo url=http://${IP}:${PORT}/
+ echo user=$USER
+ echo pass=$(rclone obscure $PASS)
+ echo _connect=${IP}:$PORT
+}
+
+. $(dirname "$0")/rclone-serve.bash
diff --git a/fstest/testserver/init.d/docker.bash b/fstest/testserver/init.d/docker.bash
new file mode 100644
index 0000000..1bcc2c2
--- /dev/null
+++ b/fstest/testserver/init.d/docker.bash
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+stop() {
+ if status ; then
+ docker stop "$NAME"
+ echo "$NAME stopped"
+ fi
+}
+
+status() {
+ if docker ps --format '{{.Names}}' | grep -q "^${NAME}$" ; then
+ echo "$NAME running"
+ else
+ echo "$NAME not running"
+ return 1
+ fi
+ return 0
+}
+
+docker_ip() {
+ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{"\n"}}{{end}}' "$NAME" | head -n 1
+}
diff --git a/fstest/testserver/init.d/rclone-serve.bash b/fstest/testserver/init.d/rclone-serve.bash
new file mode 100644
index 0000000..408960e
--- /dev/null
+++ b/fstest/testserver/init.d/rclone-serve.bash
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+
+# start an "rclone serve" server
+
+PIDFILE=/tmp/${NAME}.pid
+DATADIR=/tmp/${NAME}-data
+
+stop() {
+ if status ; then
+ pid=$(cat "$PIDFILE")
+ kill "$pid"
+ rm "$PIDFILE"
+ echo "$NAME stopped"
+ fi
+}
+
+status() {
+ if [ -e "$PIDFILE" ]; then
+ pid=$(cat "$PIDFILE")
+ if kill -0 "$pid" >/dev/null 2>&1; then
+ # echo "$NAME running"
+ return 0
+ else
+ rm "$PIDFILE"
+ fi
+ fi
+ # echo "$NAME not running"
+ return 1
+}
+
+run() {
+ if ! status ; then
+ mkdir -p "$DATADIR"
+ nohup "$@" >> "/tmp/${NAME}.log" 2>&1 </dev/null &
+ pid=$!
+ echo $pid > "$PIDFILE"
+ disown "$pid"
+ fi
+}
+
+# shellcheck disable=SC1090
+. "$(dirname "$0")/run.bash"
diff --git a/fstest/testserver/init.d/run.bash b/fstest/testserver/init.d/run.bash
new file mode 100644
index 0000000..ff1f70e
--- /dev/null
+++ b/fstest/testserver/init.d/run.bash
@@ -0,0 +1,101 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+RUN_BASE="${STATE_DIR:-${XDG_RUNTIME_DIR:-/tmp}/rclone-test-server}"
+: "${NAME:=$(basename "$0")}"
+RUN_ROOT="${RUN_BASE}/${NAME}"
+RUN_STATE="${RUN_ROOT}/state"
+RUN_LOCK_FILE="${RUN_ROOT}/lock"
+RUN_REF_COUNT="${RUN_STATE}/refcount"
+RUN_OUTPUT="${RUN_STATE}/env"
+
+mkdir -p "${RUN_STATE}"
+[[ -f "${RUN_REF_COUNT}" ]] || echo 0 >"${RUN_REF_COUNT}"
+[[ -f "${RUN_OUTPUT}" ]] || : >"${RUN_OUTPUT}"
+: > "${RUN_LOCK_FILE}" # ensure file exists
+
+# status helper that won't trip set -e
+_is_running() { set +e; status >/dev/null 2>&1; local rc=$?; set -e; return $rc; }
+
+_acquire_lock() {
+ # open fd 9 on lock file and take exclusive lock
+ exec 9>"${RUN_LOCK_FILE}"
+ flock -x 9
+}
+
+_release_lock() {
+ flock -u 9
+ exec 9>&-
+}
+
+case "${1:-}" in
+ start)
+ _acquire_lock
+ trap '_release_lock' EXIT
+
+ rc=$(cat "${RUN_REF_COUNT}" 2>/dev/null || echo 0)
+
+ if (( rc == 0 )); then
+ # First client: ensure a clean instance, then start and cache env
+ if _is_running; then
+ stop || true
+ fi
+ if ! out="$(start)"; then
+ echo "failed to start" >&2
+ exit 1
+ fi
+ printf "%s\n" "$out" > "${RUN_OUTPUT}"
+ else
+ # Already owned: make sure it’s still up; if not, restart and refresh env
+ if ! _is_running; then
+ if ! out="$(start)"; then
+ echo "failed to restart" >&2
+ exit 1
+ fi
+ printf "%s\n" "$out" > "${RUN_OUTPUT}"
+ fi
+ fi
+
+ rc=$((rc+1)); echo "${rc}" > "${RUN_REF_COUNT}"
+ cat "${RUN_OUTPUT}"
+
+ trap - EXIT
+ _release_lock
+ ;;
+
+ stop)
+ _acquire_lock
+ trap '_release_lock' EXIT
+
+ rc=$(cat "${RUN_REF_COUNT}" 2>/dev/null || echo 0)
+ if (( rc > 0 )); then rc=$((rc-1)); fi
+ echo "${rc}" > "${RUN_REF_COUNT}"
+ if (( rc == 0 )) && _is_running; then
+ stop || true
+ fi
+
+ trap - EXIT
+ _release_lock
+ ;;
+
+ reset)
+ _acquire_lock
+ trap '_release_lock' EXIT
+
+ stop || true
+ rm -rf "${RUN_BASE}"
+
+ trap - EXIT
+ _release_lock
+ ;;
+
+ status)
+ # passthrough; do NOT take the lock
+ status
+ ;;
+
+ *)
+ echo "usage: $0 {start|stop|reset|status}" >&2
+ exit 2
+ ;;
+esac
diff --git a/fstest/testserver/init.d/seafile/docker-compose.yml b/fstest/testserver/init.d/seafile/docker-compose.yml
new file mode 100644
index 0000000..8e0a099
--- /dev/null
+++ b/fstest/testserver/init.d/seafile/docker-compose.yml
@@ -0,0 +1,31 @@
+version: '2.0'
+services:
+ db:
+ image: mariadb:10.5
+ environment:
+ - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
+ - MYSQL_LOG_CONSOLE=true
+ volumes:
+ - ${SEAFILE_TEST_DATA}/${NAME}/seafile-mysql/db:/var/lib/mysql
+
+ memcached:
+ image: memcached:1.6.9
+ entrypoint: memcached -m 256
+
+ seafile:
+ image: seafileltd/seafile-mc:${SEAFILE_VERSION}
+ ports:
+ - "${SEAFILE_IP}:${SEAFILE_PORT}:80"
+ volumes:
+ - ${SEAFILE_TEST_DATA}/${NAME}/seafile-data:/shared
+ environment:
+ - DB_HOST=db
+ - DB_ROOT_PASSWD=${MYSQL_ROOT_PASSWORD}
+ - TIME_ZONE=Etc/UTC
+ - SEAFILE_ADMIN_EMAIL=${SEAFILE_ADMIN_EMAIL}
+ - SEAFILE_ADMIN_PASSWORD=${SEAFILE_ADMIN_PASSWORD}
+ - SEAFILE_SERVER_LETSENCRYPT=false
+ - SEAFILE_SERVER_HOSTNAME=${SEAFILE_IP}:${SEAFILE_PORT}
+ depends_on:
+ - db
+ - memcached
diff --git a/fstest/testserver/testserver.go b/fstest/testserver/testserver.go
new file mode 100644
index 0000000..9967bf5
--- /dev/null
+++ b/fstest/testserver/testserver.go
@@ -0,0 +1,198 @@
+// Package testserver starts and stops test servers if required
+package testserver
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/rclone/rclone/fs"
+ "github.com/rclone/rclone/fs/fspath"
+)
+
+var (
+ findConfigOnce sync.Once
+ configDir string // where the config is stored
+)
+
+// Assume we are run somewhere within the rclone root
+func findConfig() (string, error) {
+ dir := filepath.Join("fstest", "testserver", "init.d")
+ for range 5 {
+ fi, err := os.Stat(dir)
+ if err == nil && fi.IsDir() {
+ return filepath.Abs(dir)
+ } else if !os.IsNotExist(err) {
+ return "", err
+ }
+ dir = filepath.Join("..", dir)
+ }
+ return "", errors.New("couldn't find testserver config files - run from within rclone source")
+}
+
+// returns path to a script to start this server
+func cmdPath(name string) string {
+ return filepath.Join(configDir, name)
+}
+
+// return true if the server with name has a start command
+func hasStartCommand(name string) bool {
+ fi, err := os.Stat(cmdPath(name))
+ return err == nil && !fi.IsDir()
+}
+
+// run the command returning the output and an error
+func run(name, command string) (out []byte, err error) {
+ script := cmdPath(name)
+ cmd := exec.Command(script, command)
+ out, err = cmd.CombinedOutput()
+ if err != nil {
+ err = fmt.Errorf("failed to run %s %s\n%s: %w", script, command, string(out), err)
+ }
+ return out, err
+}
+
+// envKey returns the environment variable name to set name, key
+func envKey(name, key string) string {
+ return fmt.Sprintf("RCLONE_CONFIG_%s_%s", strings.ToUpper(name), strings.ToUpper(key))
+}
+
+// match a line of config var=value
+var matchLine = regexp.MustCompile(`^([a-zA-Z_]+)=(.*)$`)
+
+// Start the server and env vars so rclone can use it
+func start(name string) error {
+ fs.Logf(name, "Starting server")
+ out, err := run(name, "start")
+ if err != nil {
+ return err
+ }
+ // parse the output and set environment vars from it
+ var connect string
+ var connectDelay time.Duration
+ for line := range bytes.SplitSeq(out, []byte("\n")) {
+ line = bytes.TrimSpace(line)
+ part := matchLine.FindSubmatch(line)
+ if part != nil {
+ key, value := part[1], part[2]
+ if string(key) == "_connect" {
+ connect = string(value)
+ continue
+ } else if string(key) == "_connect_delay" {
+ connectDelay, err = time.ParseDuration(string(value))
+ if err != nil {
+ return fmt.Errorf("bad _connect_delay: %w", err)
+ }
+ continue
+ }
+
+ // fs.Debugf(name, "key = %q, envKey = %q, value = %q", key, envKey(name, string(key)), value)
+ err = os.Setenv(envKey(name, string(key)), string(value))
+ if err != nil {
+ return err
+ }
+ }
+ }
+ if connect == "" {
+ fs.Logf(name, "Started server")
+ return nil
+ }
+ // If we got a _connect value then try to connect to it
+ const maxTries = 100
+ var rdBuf = make([]byte, 1)
+ for i := 1; i <= maxTries; i++ {
+ if i != 0 {
+ time.Sleep(time.Second)
+ }
+ fs.Logf(name, "Attempting to connect to %q try %d/%d", connect, i, maxTries)
+ conn, err := net.DialTimeout("tcp", connect, time.Second)
+ if err != nil {
+ fs.Debugf(name, "Connection to %q failed try %d/%d: %v", connect, i, maxTries, err)
+ continue
+ }
+
+ err = conn.SetReadDeadline(time.Now().Add(time.Second))
+ if err != nil {
+ return fmt.Errorf("failed to set deadline: %w", err)
+ }
+ n, err := conn.Read(rdBuf)
+ _ = conn.Close()
+ fs.Debugf(name, "Read %d, error: %v", n, err)
+ if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) {
+ // Try again
+ continue
+ }
+ if connectDelay > 0 {
+ fs.Logf(name, "Connect delay %v", connectDelay)
+ time.Sleep(connectDelay)
+ }
+ fs.Logf(name, "Started server and connected to %q", connect)
+ return nil
+ }
+ return fmt.Errorf("failed to connect to %q on %q", name, connect)
+}
+
+// Stops the named test server
+func stop(name string) {
+ fs.Logf(name, "Stopping server")
+ _, err := run(name, "stop")
+ if err != nil {
+ fs.Errorf(name, "Failed to stop server: %v", err)
+ }
+}
+
+// No server to stop so do nothing
+func stopNothing() {
+}
+
+// Start starts the test server for remoteName.
+//
+// This must be stopped by calling the function returned when finished.
+func Start(remote string) (fn func(), err error) {
+ // don't start the local backend
+ if remote == "" {
+ return stopNothing, nil
+ }
+ parsed, err := fspath.Parse(remote)
+ if err != nil {
+ return nil, err
+ }
+ name := parsed.ConfigString
+ // don't start the local backend
+ if name == "" {
+ return stopNothing, nil
+ }
+
+ // Make sure we know where the config is
+ findConfigOnce.Do(func() {
+ configDir, err = findConfig()
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // If remote has no start command then do nothing
+ if !hasStartCommand(name) {
+ return stopNothing, nil
+ }
+
+ // Start the server
+ err = start(name)
+ if err != nil {
+ return nil, err
+ }
+
+ // And return a function to stop it
+ return func() {
+ stop(name)
+ }, nil
+
+}