From ad53429cf18bd4e75124aa7fa137a0a70e66f011 Mon Sep 17 00:00:00 2001
From: Nicola Murino <nicola.murino@gmail.com>
Date: Sat, 23 May 2020 11:58:05 +0200
Subject: [PATCH] add support for build tag to allow to disable some features

The following build tags are available:

- "nogcs", disable Google Cloud Storage backend
- "nos3", disable S3 Compabible Object Storage backends
- "nobolt", disable Bolt data provider
- "nomysql", disable MySQL data provider
- "nopgsql", disable PostgreSQL data provider
- "nosqlite", disable SQLite data provider
- "noportable", disable portable mode
---
 cmd/portable.go                 |   5 ++
 cmd/portable_disabled.go        |   9 +++
 cmd/root.go                     |   2 +-
 dataprovider/bolt.go            |   6 ++
 dataprovider/bolt_disabled.go   |  17 +++++
 dataprovider/mysql.go           |  10 +++
 dataprovider/mysql_disabled.go  |  17 +++++
 dataprovider/pgsql.go           |  10 +++
 dataprovider/pgsql_disabled.go  |  17 +++++
 dataprovider/sqlite.go          |   9 +++
 dataprovider/sqlite_disabled.go |  17 +++++
 docker/sftpgo/alpine/Dockerfile |   9 +--
 docker/sftpgo/alpine/README.md  |   5 +-
 docker/sftpgo/debian/Dockerfile |   8 +-
 docker/sftpgo/debian/README.md  |  13 +++-
 docs/build-from-source.md       |  24 ++++--
 httpd/schema/openapi.yaml       |   7 +-
 main.go                         |   8 +-
 service/service.go              | 112 ----------------------------
 service/service_portable.go     | 126 ++++++++++++++++++++++++++++++++
 utils/version.go                |  29 ++++++--
 vfs/gcsfs.go                    |  23 ++----
 vfs/gcsfs_disabled.go           |  18 +++++
 vfs/s3fs.go                     |  33 ++-------
 vfs/s3fs_disabled.go            |  18 +++++
 vfs/vfs.go                      |  43 +++++++++++
 26 files changed, 406 insertions(+), 189 deletions(-)
 create mode 100644 cmd/portable_disabled.go
 create mode 100644 dataprovider/bolt_disabled.go
 create mode 100644 dataprovider/mysql_disabled.go
 create mode 100644 dataprovider/pgsql_disabled.go
 create mode 100644 dataprovider/sqlite_disabled.go
 create mode 100644 service/service_portable.go
 create mode 100644 vfs/gcsfs_disabled.go
 create mode 100644 vfs/s3fs_disabled.go

diff --git a/cmd/portable.go b/cmd/portable.go
index 92bd8f93..bfc634c1 100644
--- a/cmd/portable.go
+++ b/cmd/portable.go
@@ -1,3 +1,5 @@
+// +build !noportable
+
 package cmd
 
 import (
@@ -14,6 +16,7 @@ import (
 	"github.com/drakkan/sftpgo/dataprovider"
 	"github.com/drakkan/sftpgo/service"
 	"github.com/drakkan/sftpgo/sftpd"
+	"github.com/drakkan/sftpgo/utils"
 	"github.com/drakkan/sftpgo/vfs"
 )
 
@@ -138,6 +141,8 @@ Please take a look at the usage below to customize the serving parameters`,
 )
 
 func init() {
+	utils.AddFeature("+portable")
+
 	portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".",
 		"Path to the directory to serve. This can be an absolute path or a path relative to the current directory")
 	portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, "0 means a random non privileged port")
diff --git a/cmd/portable_disabled.go b/cmd/portable_disabled.go
new file mode 100644
index 00000000..c374cae3
--- /dev/null
+++ b/cmd/portable_disabled.go
@@ -0,0 +1,9 @@
+// +build noportable
+
+package cmd
+
+import "github.com/drakkan/sftpgo/utils"
+
+func init() {
+	utils.AddFeature("-portable")
+}
diff --git a/cmd/root.go b/cmd/root.go
index 7b9bd617..f1007eae 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -63,7 +63,7 @@ func init() {
 	version := utils.GetAppVersion()
 	rootCmd.Flags().BoolP("version", "v", false, "")
 	rootCmd.Version = version.GetVersionAsString()
-	rootCmd.SetVersionTemplate(`{{printf "SFTPGo version: "}}{{printf "%s" .Version}}
+	rootCmd.SetVersionTemplate(`{{printf "SFTPGo "}}{{printf "%s" .Version}}
 `)
 }
 
diff --git a/dataprovider/bolt.go b/dataprovider/bolt.go
index d93cba28..8097c4aa 100644
--- a/dataprovider/bolt.go
+++ b/dataprovider/bolt.go
@@ -1,3 +1,5 @@
+// +build !nobolt
+
 package dataprovider
 
 import (
@@ -52,6 +54,10 @@ type compatUserV2 struct {
 	Status            int      `json:"status"`
 }
 
+func init() {
+	utils.AddFeature("+bolt")
+}
+
 func initializeBoltProvider(basePath string) error {
 	var err error
 	logSender = fmt.Sprintf("dataprovider_%v", BoltDataProviderName)
diff --git a/dataprovider/bolt_disabled.go b/dataprovider/bolt_disabled.go
new file mode 100644
index 00000000..a84260e2
--- /dev/null
+++ b/dataprovider/bolt_disabled.go
@@ -0,0 +1,17 @@
+// +build nobolt
+
+package dataprovider
+
+import (
+	"errors"
+
+	"github.com/drakkan/sftpgo/utils"
+)
+
+func init() {
+	utils.AddFeature("-bolt")
+}
+
+func initializeBoltProvider(basePath string) error {
+	return errors.New("bolt disabled at build time")
+}
diff --git a/dataprovider/mysql.go b/dataprovider/mysql.go
index b02d71bc..edfb2bf1 100644
--- a/dataprovider/mysql.go
+++ b/dataprovider/mysql.go
@@ -1,3 +1,5 @@
+// +build !nomysql
+
 package dataprovider
 
 import (
@@ -6,7 +8,11 @@ import (
 	"strings"
 	"time"
 
+	// we import go-sql-driver/mysql here to be able to disable MySQL support using a build tag
+	_ "github.com/go-sql-driver/mysql"
+
 	"github.com/drakkan/sftpgo/logger"
+	"github.com/drakkan/sftpgo/utils"
 )
 
 const (
@@ -28,6 +34,10 @@ type MySQLProvider struct {
 	dbHandle *sql.DB
 }
 
+func init() {
+	utils.AddFeature("+mysql")
+}
+
 func initializeMySQLProvider() error {
 	var err error
 	logSender = fmt.Sprintf("dataprovider_%v", MySQLDataProviderName)
diff --git a/dataprovider/mysql_disabled.go b/dataprovider/mysql_disabled.go
new file mode 100644
index 00000000..e004be9f
--- /dev/null
+++ b/dataprovider/mysql_disabled.go
@@ -0,0 +1,17 @@
+// +build nomysql
+
+package dataprovider
+
+import (
+	"errors"
+
+	"github.com/drakkan/sftpgo/utils"
+)
+
+func init() {
+	utils.AddFeature("-mysql")
+}
+
+func initializeMySQLProvider() error {
+	return errors.New("MySQL disabled at build time")
+}
diff --git a/dataprovider/pgsql.go b/dataprovider/pgsql.go
index 2793569a..b5ef445e 100644
--- a/dataprovider/pgsql.go
+++ b/dataprovider/pgsql.go
@@ -1,3 +1,5 @@
+// +build !nopgsql
+
 package dataprovider
 
 import (
@@ -5,7 +7,11 @@ import (
 	"fmt"
 	"strings"
 
+	// we import lib/pq here to be able to disable PostgreSQL support using a build tag
+	_ "github.com/lib/pq"
+
 	"github.com/drakkan/sftpgo/logger"
+	"github.com/drakkan/sftpgo/utils"
 )
 
 const (
@@ -26,6 +32,10 @@ type PGSQLProvider struct {
 	dbHandle *sql.DB
 }
 
+func init() {
+	utils.AddFeature("+pgsql")
+}
+
 func initializePGSQLProvider() error {
 	var err error
 	logSender = fmt.Sprintf("dataprovider_%v", PGSQLDataProviderName)
diff --git a/dataprovider/pgsql_disabled.go b/dataprovider/pgsql_disabled.go
new file mode 100644
index 00000000..c9aeee32
--- /dev/null
+++ b/dataprovider/pgsql_disabled.go
@@ -0,0 +1,17 @@
+// +build nopgsql
+
+package dataprovider
+
+import (
+	"errors"
+
+	"github.com/drakkan/sftpgo/utils"
+)
+
+func init() {
+	utils.AddFeature("-pgsql")
+}
+
+func initializePGSQLProvider() error {
+	return errors.New("PostgreSQL disabled at build time")
+}
diff --git a/dataprovider/sqlite.go b/dataprovider/sqlite.go
index 16f86904..2556e08d 100644
--- a/dataprovider/sqlite.go
+++ b/dataprovider/sqlite.go
@@ -1,3 +1,5 @@
+// +build !nosqlite
+
 package dataprovider
 
 import (
@@ -6,6 +8,9 @@ import (
 	"path/filepath"
 	"strings"
 
+	// we import go-sqlite3 here to be able to disable SQLite support using a build tag
+	_ "github.com/mattn/go-sqlite3"
+
 	"github.com/drakkan/sftpgo/logger"
 	"github.com/drakkan/sftpgo/utils"
 )
@@ -41,6 +46,10 @@ type SQLiteProvider struct {
 	dbHandle *sql.DB
 }
 
+func init() {
+	utils.AddFeature("+sqlite")
+}
+
 func initializeSQLiteProvider(basePath string) error {
 	var err error
 	var connectionString string
diff --git a/dataprovider/sqlite_disabled.go b/dataprovider/sqlite_disabled.go
new file mode 100644
index 00000000..430f2a72
--- /dev/null
+++ b/dataprovider/sqlite_disabled.go
@@ -0,0 +1,17 @@
+// +build nosqlite
+
+package dataprovider
+
+import (
+	"errors"
+
+	"github.com/drakkan/sftpgo/utils"
+)
+
+func init() {
+	utils.AddFeature("-sqlite")
+}
+
+func initializeSQLiteProvider(basePath string) error {
+	return errors.New("SQLite disabled at build time")
+}
diff --git a/docker/sftpgo/alpine/Dockerfile b/docker/sftpgo/alpine/Dockerfile
index 46952469..77a5cdd6 100644
--- a/docker/sftpgo/alpine/Dockerfile
+++ b/docker/sftpgo/alpine/Dockerfile
@@ -4,19 +4,18 @@ RUN apk add --no-cache git gcc g++ ca-certificates \
   && go get -d github.com/drakkan/sftpgo
 WORKDIR /go/src/github.com/drakkan/sftpgo
 ARG TAG
+ARG FEATURES
 # Use --build-arg TAG=LATEST for latest tag. Use e.g. --build-arg TAG=0.9.6 for a specific tag/commit. Otherwise HEAD (master) is built.
 RUN git checkout $(if [ "${TAG}" = LATEST ]; then echo `git rev-list --tags --max-count=1`; elif [ -n "${TAG}" ]; then echo "${TAG}"; else echo HEAD; fi)
-RUN go build -i -ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date -u +%FT%TZ`" -o /go/bin/sftpgo
+RUN go build -i $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date -u +%FT%TZ`" -o /go/bin/sftpgo
 
 FROM alpine:latest
 
 RUN apk add --no-cache ca-certificates su-exec \
   && mkdir -p /data /etc/sftpgo /srv/sftpgo/config /srv/sftpgo/web /srv/sftpgo/backups
 
-# ca-certificates is needed for Cloud Storage Support and to expose the REST API over HTTPS.
-# If you install git then ca-certificates will be automatically installed as dependency.
-# git, rsync and ca-certificates are optional, uncomment the next line to add support for them if needed.
-#RUN apk add --no-cache git rsync ca-certificates
+# git and rsync are optional, uncomment the next line to add support for them if needed.
+#RUN apk add --no-cache git rsync
 
 COPY --from=builder /go/bin/sftpgo /bin/
 COPY --from=builder /go/src/github.com/drakkan/sftpgo/sftpgo.json /etc/sftpgo/sftpgo.json
diff --git a/docker/sftpgo/alpine/README.md b/docker/sftpgo/alpine/README.md
index 603ff373..2a955ce9 100644
--- a/docker/sftpgo/alpine/README.md
+++ b/docker/sftpgo/alpine/README.md
@@ -13,7 +13,10 @@ sudo groupadd -g 1003 sftpgrp && \
 
 # Edit sftpgo.json as you need
 
-# Get and build SFTPGo image (add --build-arg TAG=LATEST to build the latest tag or e.g. TAG=0.9.6 for a specific tag/commit).
+# Get and build SFTPGo image.
+# Add --build-arg TAG=LATEST to build the latest tag or e.g. TAG=0.9.6 for a specific tag/commit.
+# Add --build-arg FEATURES=<features to disable> to disable some feature.
+# Please take a look at the [build from source](./../../../docs/build-from-source.md) documentation for the complete list of the features that can be disabled.
 git clone https://github.com/drakkan/sftpgo.git && \
   cd sftpgo && \
   sudo docker build -t sftpgo docker/sftpgo/alpine/
diff --git a/docker/sftpgo/debian/Dockerfile b/docker/sftpgo/debian/Dockerfile
index 5aac420f..5e6ef93e 100644
--- a/docker/sftpgo/debian/Dockerfile
+++ b/docker/sftpgo/debian/Dockerfile
@@ -4,16 +4,18 @@ LABEL maintainer="nicola.murino@gmail.com"
 RUN go get -d github.com/drakkan/sftpgo
 WORKDIR /go/src/github.com/drakkan/sftpgo
 ARG TAG
+ARG FEATURES
 # Use --build-arg TAG=LATEST for latest tag. Use e.g. --build-arg TAG=0.9.6 for a specific tag/commit. Otherwise HEAD (master) is built.
 RUN git checkout $(if [ "${TAG}" = LATEST ]; then echo `git rev-list --tags --max-count=1`; elif [ -n "${TAG}" ]; then echo "${TAG}"; else echo HEAD; fi)
-RUN go build -i -ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date -u +%FT%TZ`" -o sftpgo
+RUN go build -i $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date -u +%FT%TZ`" -o sftpgo
 
 # now define the run environment
 FROM debian:latest
 
 # ca-certificates is needed for Cloud Storage Support and to expose the REST API over HTTPS.
-# If you install git then ca-certificates will be automatically installed as dependency.
-# git, rsync and ca-certificates are optional, uncomment the next line to add support for them if needed.
+RUN apt-get update && apt-get install -y ca-certificates
+
+# git and rsync are optional, uncomment the next line to add support for them if needed.
 #RUN apt-get update && apt-get install -y git rsync ca-certificates
 
 ARG BASE_DIR=/app
diff --git a/docker/sftpgo/debian/README.md b/docker/sftpgo/debian/README.md
index 65c0325a..7ea154b6 100644
--- a/docker/sftpgo/debian/README.md
+++ b/docker/sftpgo/debian/README.md
@@ -8,13 +8,22 @@ You can build the container image using `docker build`, for example:
 docker build -t="drakkan/sftpgo" .
 ```
 
-This will build master of github.com/drakkan/sftpgo. To build the latest tag you can add `--build-arg TAG=LATEST`
-and to build a specific tag/commit you can use for example `TAG=0.9.6`, like this:
+This will build master of github.com/drakkan/sftpgo.
+
+To build the latest tag you can add `--build-arg TAG=LATEST` and to build a specific tag/commit you can use for example `TAG=0.9.6`, like this:
 
 ```bash
 docker build -t="drakkan/sftpgo" --build-arg TAG=0.9.6 .
 ```
 
+To disable some features you can add `--build-arg FEATURES=<features to disable>`. For example you can disable SQLite support like this:
+
+```bash
+docker build -t="drakkan/sftpgo" --build-arg FEATURES=nosqlite .
+```
+
+Please take a look at the [build from source](./../../../docs/build-from-source.md) documentation for the complete list of the features that can be disabled.
+
 Now create the required folders on the host system, for example:
 
 ```bash
diff --git a/docs/build-from-source.md b/docs/build-from-source.md
index ad26d97c..6e2a46d7 100644
--- a/docs/build-from-source.md
+++ b/docs/build-from-source.md
@@ -8,13 +8,23 @@ go get -u github.com/drakkan/sftpgo
 
 Make sure [Git](https://git-scm.com/downloads) is installed on your machine and in your system's `PATH`.
 
-SFTPGo depends on [go-sqlite3](https://github.com/mattn/go-sqlite3) which is a CGO package and so it requires a `C` compiler at build time.
+The following build tags are available to disable some features:
+
+- `nogcs`, disable Google Cloud Storage backend
+- `nos3`, disable S3 Compabible Object Storage backends
+- `nobolt`, disable Bolt data provider
+- `nomysql`, disable MySQL data provider
+- `nopgsql`, disable PostgreSQL data provider
+- `nosqlite`, disable SQLite data provider
+- `noportable`, disable portable mode
+
+If no build tag is specified all the features will be included.
+
+The optional [SQLite driver](https://github.com/mattn/go-sqlite3 "go-sqlite3") is a `CGO` package and so it requires a `C` compiler at build time.
 On Linux and macOS, a compiler is easy to install or already installed. On Windows, you need to download [MinGW-w64](https://sourceforge.net/projects/mingw-w64/files/) and build SFTPGo from its command prompt.
 
 The compiler is a build time only dependency. It is not required at runtime.
 
-If you don't need SQLite, you can also get/build SFTPGo setting the environment variable `GCO_ENABLED` to 0. This way, SQLite support will be disabled and PostgreSQL, MySQL, bbolt and memory data providers will keep working. In this way, you don't need a `C` compiler for building.
-
 Version info, such as git commit and build date, can be embedded setting the following string variables at build time:
 
 - `github.com/drakkan/sftpgo/utils.commit`
@@ -23,12 +33,12 @@ Version info, such as git commit and build date, can be embedded setting the fol
 For example, you can build using the following command:
 
 ```bash
-go build -i -ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date -u +%FT%TZ`" -o sftpgo
+go build -i -tags nogcs,nos3,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date -u +%FT%TZ`" -o sftpgo
 ```
 
-You should get a version that includes git commit and build date like this one:
+You should get a version that includes git commit, build date and available features like this one:
 
 ```bash
-$ sftpgo -v
-SFTPGo version: 0.9.0-dev-90607d4-dirty-2019-08-08T19:28:36Z
+$ ./sftpgo -v
+SFTPGo 0.9.6-dev-15298b0-dirty-2020-05-22T21:25:51Z -gcs -s3 +bolt +mysql +pgsql -sqlite +portable
 ```
\ No newline at end of file
diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml
index dcc2f7c4..eb695799 100644
--- a/httpd/schema/openapi.yaml
+++ b/httpd/schema/openapi.yaml
@@ -2,7 +2,7 @@ openapi: 3.0.1
 info:
   title: SFTPGo
   description: 'SFTPGo REST API'
-  version: 1.8.5
+  version: 1.8.6
 
 servers:
 - url: /api/v1
@@ -1278,6 +1278,11 @@ components:
           type: string
         commit_hash:
           type: string
+        features:
+          type: array
+          items:
+            type: string
+          description: Features for the current build. Available features are "portable", "bolt", "mysql", "sqlite", "pgsql", "s3", "gcs". If a feature is available it has a "+" prefix, otherwise a "-" prefix
   securitySchemes:
     BasicAuth:
       type: http
diff --git a/main.go b/main.go
index 2144df49..444edef3 100644
--- a/main.go
+++ b/main.go
@@ -3,13 +3,7 @@
 // https://github.com/drakkan/sftpgo/blob/master/README.md
 package main // import "github.com/drakkan/sftpgo"
 
-import (
-	_ "github.com/go-sql-driver/mysql"
-	_ "github.com/lib/pq"
-	_ "github.com/mattn/go-sqlite3"
-
-	"github.com/drakkan/sftpgo/cmd"
-)
+import "github.com/drakkan/sftpgo/cmd"
 
 func main() {
 	cmd.Execute()
diff --git a/service/service.go b/service/service.go
index 923ecf6b..ccc770f4 100644
--- a/service/service.go
+++ b/service/service.go
@@ -2,16 +2,8 @@
 package service
 
 import (
-	"fmt"
-	"math/rand"
-	"os"
-	"os/signal"
 	"path/filepath"
-	"strings"
-	"syscall"
-	"time"
 
-	"github.com/grandcat/zeroconf"
 	"github.com/rs/zerolog"
 
 	"github.com/drakkan/sftpgo/config"
@@ -141,107 +133,3 @@ func (s *Service) Stop() {
 	close(s.Shutdown)
 	logger.Debug(logSender, "", "Service stopped")
 }
-
-// StartPortableMode starts the service in portable mode
-func (s *Service) StartPortableMode(sftpdPort int, enabledSSHCommands []string, advertiseService, advertiseCredentials bool) error {
-	if s.PortableMode != 1 {
-		return fmt.Errorf("service is not configured for portable mode")
-	}
-	var err error
-	rand.Seed(time.Now().UnixNano())
-	if len(s.PortableUser.Username) == 0 {
-		s.PortableUser.Username = "user"
-	}
-	if len(s.PortableUser.PublicKeys) == 0 && len(s.PortableUser.Password) == 0 {
-		var b strings.Builder
-		for i := 0; i < 8; i++ {
-			b.WriteRune(chars[rand.Intn(len(chars))])
-		}
-		s.PortableUser.Password = b.String()
-	}
-	dataProviderConf := config.GetProviderConf()
-	dataProviderConf.Driver = dataprovider.MemoryDataProviderName
-	dataProviderConf.Name = ""
-	dataProviderConf.CredentialsPath = filepath.Join(os.TempDir(), "credentials")
-	config.SetProviderConf(dataProviderConf)
-	httpdConf := config.GetHTTPDConfig()
-	httpdConf.BindPort = 0
-	config.SetHTTPDConfig(httpdConf)
-	sftpdConf := config.GetSFTPDConfig()
-	sftpdConf.MaxAuthTries = 12
-	if sftpdPort > 0 {
-		sftpdConf.BindPort = sftpdPort
-	} else {
-		// dynamic ports starts from 49152
-		sftpdConf.BindPort = 49152 + rand.Intn(15000)
-	}
-	if utils.IsStringInSlice("*", enabledSSHCommands) {
-		sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
-	} else {
-		sftpdConf.EnabledSSHCommands = enabledSSHCommands
-	}
-	config.SetSFTPDConfig(sftpdConf)
-
-	err = s.Start()
-	if err != nil {
-		return err
-	}
-	var mDNSService *zeroconf.Server
-	if advertiseService {
-		version := utils.GetAppVersion()
-		meta := []string{
-			fmt.Sprintf("version=%v", version.GetVersionAsString()),
-		}
-		if advertiseCredentials {
-			logger.InfoToConsole("Advertising credentials via multicast DNS")
-			meta = append(meta, fmt.Sprintf("user=%v", s.PortableUser.Username))
-			if len(s.PortableUser.Password) > 0 {
-				meta = append(meta, fmt.Sprintf("password=%v", s.PortableUser.Password))
-			} else {
-				logger.InfoToConsole("Unable to advertise key based credentials via multicast DNS, we don't have the private key")
-			}
-		}
-		mDNSService, err = zeroconf.Register(
-			fmt.Sprintf("SFTPGo portable %v", sftpdConf.BindPort), // service instance name
-			"_sftp-ssh._tcp",   // service type and protocol
-			"local.",           // service domain
-			sftpdConf.BindPort, // service port
-			meta,               // service metadata
-			nil,                // register on all network interfaces
-		)
-		if err != nil {
-			mDNSService = nil
-			logger.WarnToConsole("Unable to advertise SFTP service via multicast DNS: %v", err)
-		} else {
-			logger.InfoToConsole("SFTP service advertised via multicast DNS")
-		}
-	}
-	sig := make(chan os.Signal, 1)
-	signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
-	go func() {
-		<-sig
-		if mDNSService != nil {
-			logger.InfoToConsole("unregistering multicast DNS service")
-			mDNSService.Shutdown()
-		}
-		s.Stop()
-	}()
-
-	logger.InfoToConsole("Portable mode ready, SFTP port: %v, user: %#v, password: %#v, public keys: %v, directory: %#v, "+
-		"permissions: %+v, enabled ssh commands: %v file extensions filters: %+v", sftpdConf.BindPort, s.PortableUser.Username,
-		s.PortableUser.Password, s.PortableUser.PublicKeys, s.getPortableDirToServe(), s.PortableUser.Permissions,
-		sftpdConf.EnabledSSHCommands, s.PortableUser.Filters.FileExtensions)
-	return nil
-}
-
-func (s *Service) getPortableDirToServe() string {
-	var dirToServe string
-	if s.PortableUser.FsConfig.Provider == 1 {
-		dirToServe = s.PortableUser.FsConfig.S3Config.KeyPrefix
-	} else if s.PortableUser.FsConfig.Provider == 2 {
-		dirToServe = s.PortableUser.FsConfig.GCSConfig.KeyPrefix
-	} else {
-		dirToServe = s.PortableUser.HomeDir
-	}
-	return dirToServe
-}
diff --git a/service/service_portable.go b/service/service_portable.go
new file mode 100644
index 00000000..b05b50f6
--- /dev/null
+++ b/service/service_portable.go
@@ -0,0 +1,126 @@
+// +build !noportable
+
+package service
+
+import (
+	"fmt"
+	"math/rand"
+	"os"
+	"os/signal"
+	"path/filepath"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/grandcat/zeroconf"
+
+	"github.com/drakkan/sftpgo/config"
+	"github.com/drakkan/sftpgo/dataprovider"
+	"github.com/drakkan/sftpgo/logger"
+	"github.com/drakkan/sftpgo/sftpd"
+	"github.com/drakkan/sftpgo/utils"
+)
+
+// StartPortableMode starts the service in portable mode
+func (s *Service) StartPortableMode(sftpdPort int, enabledSSHCommands []string, advertiseService, advertiseCredentials bool) error {
+	if s.PortableMode != 1 {
+		return fmt.Errorf("service is not configured for portable mode")
+	}
+	var err error
+	rand.Seed(time.Now().UnixNano())
+	if len(s.PortableUser.Username) == 0 {
+		s.PortableUser.Username = "user"
+	}
+	if len(s.PortableUser.PublicKeys) == 0 && len(s.PortableUser.Password) == 0 {
+		var b strings.Builder
+		for i := 0; i < 8; i++ {
+			b.WriteRune(chars[rand.Intn(len(chars))])
+		}
+		s.PortableUser.Password = b.String()
+	}
+	dataProviderConf := config.GetProviderConf()
+	dataProviderConf.Driver = dataprovider.MemoryDataProviderName
+	dataProviderConf.Name = ""
+	dataProviderConf.CredentialsPath = filepath.Join(os.TempDir(), "credentials")
+	config.SetProviderConf(dataProviderConf)
+	httpdConf := config.GetHTTPDConfig()
+	httpdConf.BindPort = 0
+	config.SetHTTPDConfig(httpdConf)
+	sftpdConf := config.GetSFTPDConfig()
+	sftpdConf.MaxAuthTries = 12
+	if sftpdPort > 0 {
+		sftpdConf.BindPort = sftpdPort
+	} else {
+		// dynamic ports starts from 49152
+		sftpdConf.BindPort = 49152 + rand.Intn(15000)
+	}
+	if utils.IsStringInSlice("*", enabledSSHCommands) {
+		sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
+	} else {
+		sftpdConf.EnabledSSHCommands = enabledSSHCommands
+	}
+	config.SetSFTPDConfig(sftpdConf)
+
+	err = s.Start()
+	if err != nil {
+		return err
+	}
+	var mDNSService *zeroconf.Server
+	if advertiseService {
+		version := utils.GetAppVersion()
+		meta := []string{
+			fmt.Sprintf("version=%v", version.GetVersionAsString()),
+		}
+		if advertiseCredentials {
+			logger.InfoToConsole("Advertising credentials via multicast DNS")
+			meta = append(meta, fmt.Sprintf("user=%v", s.PortableUser.Username))
+			if len(s.PortableUser.Password) > 0 {
+				meta = append(meta, fmt.Sprintf("password=%v", s.PortableUser.Password))
+			} else {
+				logger.InfoToConsole("Unable to advertise key based credentials via multicast DNS, we don't have the private key")
+			}
+		}
+		mDNSService, err = zeroconf.Register(
+			fmt.Sprintf("SFTPGo portable %v", sftpdConf.BindPort), // service instance name
+			"_sftp-ssh._tcp",   // service type and protocol
+			"local.",           // service domain
+			sftpdConf.BindPort, // service port
+			meta,               // service metadata
+			nil,                // register on all network interfaces
+		)
+		if err != nil {
+			mDNSService = nil
+			logger.WarnToConsole("Unable to advertise SFTP service via multicast DNS: %v", err)
+		} else {
+			logger.InfoToConsole("SFTP service advertised via multicast DNS")
+		}
+	}
+	sig := make(chan os.Signal, 1)
+	signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-sig
+		if mDNSService != nil {
+			logger.InfoToConsole("unregistering multicast DNS service")
+			mDNSService.Shutdown()
+		}
+		s.Stop()
+	}()
+
+	logger.InfoToConsole("Portable mode ready, SFTP port: %v, user: %#v, password: %#v, public keys: %v, directory: %#v, "+
+		"permissions: %+v, enabled ssh commands: %v file extensions filters: %+v", sftpdConf.BindPort, s.PortableUser.Username,
+		s.PortableUser.Password, s.PortableUser.PublicKeys, s.getPortableDirToServe(), s.PortableUser.Permissions,
+		sftpdConf.EnabledSSHCommands, s.PortableUser.Filters.FileExtensions)
+	return nil
+}
+
+func (s *Service) getPortableDirToServe() string {
+	var dirToServe string
+	if s.PortableUser.FsConfig.Provider == 1 {
+		dirToServe = s.PortableUser.FsConfig.S3Config.KeyPrefix
+	} else if s.PortableUser.FsConfig.Provider == 2 {
+		dirToServe = s.PortableUser.FsConfig.GCSConfig.KeyPrefix
+	} else {
+		dirToServe = s.PortableUser.HomeDir
+	}
+	return dirToServe
+}
diff --git a/utils/version.go b/utils/version.go
index 57f385bf..e475ccb4 100644
--- a/utils/version.go
+++ b/utils/version.go
@@ -1,5 +1,7 @@
 package utils
 
+import "strings"
+
 const version = "0.9.6-dev"
 
 var (
@@ -10,21 +12,34 @@ var (
 
 // VersionInfo defines version details
 type VersionInfo struct {
-	Version    string `json:"version"`
-	BuildDate  string `json:"build_date"`
-	CommitHash string `json:"commit_hash"`
+	Version    string   `json:"version"`
+	BuildDate  string   `json:"build_date"`
+	CommitHash string   `json:"commit_hash"`
+	Features   []string `json:"features"`
 }
 
 // GetVersionAsString returns the string representation of the VersionInfo struct
 func (v *VersionInfo) GetVersionAsString() string {
-	versionString := v.Version
+	var sb strings.Builder
+	sb.WriteString(v.Version)
 	if len(v.CommitHash) > 0 {
-		versionString += "-" + v.CommitHash
+		sb.WriteString("-")
+		sb.WriteString(v.CommitHash)
 	}
 	if len(v.BuildDate) > 0 {
-		versionString += "-" + v.BuildDate
+		sb.WriteString("-")
+		sb.WriteString(v.BuildDate)
 	}
-	return versionString
+	if len(v.Features) > 0 {
+		sb.WriteString(" ")
+		sb.WriteString(strings.Join(v.Features, " "))
+	}
+	return sb.String()
+}
+
+// AddFeature adds a feature description
+func AddFeature(feature string) {
+	versionInfo.Features = append(versionInfo.Features, feature)
 }
 
 func init() {
diff --git a/vfs/gcsfs.go b/vfs/gcsfs.go
index cd4d22ad..dcb7a4d3 100644
--- a/vfs/gcsfs.go
+++ b/vfs/gcsfs.go
@@ -1,3 +1,5 @@
+// +build !nogcs
+
 package vfs
 
 import (
@@ -19,6 +21,7 @@ import (
 
 	"github.com/drakkan/sftpgo/logger"
 	"github.com/drakkan/sftpgo/metrics"
+	"github.com/drakkan/sftpgo/utils"
 )
 
 var (
@@ -27,22 +30,6 @@ var (
 	gcsDefaultFieldsSelection = []string{"Name", "Size", "Deleted", "Updated"}
 )
 
-// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
-type GCSFsConfig struct {
-	Bucket string `json:"bucket,omitempty"`
-	// KeyPrefix is similar to a chroot directory for local filesystem.
-	// If specified the SFTP user will only see objects that starts with
-	// this prefix and so you can restrict access to a specific virtual
-	// folder. The prefix, if not empty, must not start with "/" and must
-	// end with "/".
-	// If empty the whole bucket contents will be available
-	KeyPrefix            string `json:"key_prefix,omitempty"`
-	CredentialFile       string `json:"-"`
-	Credentials          string `json:"credentials,omitempty"`
-	AutomaticCredentials int    `json:"automatic_credentials,omitempty"`
-	StorageClass         string `json:"storage_class,omitempty"`
-}
-
 // GCSFs is a Fs implementation for Google Cloud Storage.
 type GCSFs struct {
 	connectionID   string
@@ -53,6 +40,10 @@ type GCSFs struct {
 	ctxLongTimeout time.Duration
 }
 
+func init() {
+	utils.AddFeature("+gcs")
+}
+
 // NewGCSFs returns an GCSFs object that allows to interact with Google Cloud Storage
 func NewGCSFs(connectionID, localTempDir string, config GCSFsConfig) (Fs, error) {
 	var err error
diff --git a/vfs/gcsfs_disabled.go b/vfs/gcsfs_disabled.go
new file mode 100644
index 00000000..f1515e2b
--- /dev/null
+++ b/vfs/gcsfs_disabled.go
@@ -0,0 +1,18 @@
+// +build nogcs
+
+package vfs
+
+import (
+	"errors"
+
+	"github.com/drakkan/sftpgo/utils"
+)
+
+func init() {
+	utils.AddFeature("-gcs")
+}
+
+// NewGCSFs returns an error, GCS is disabled
+func NewGCSFs(connectionID, localTempDir string, config GCSFsConfig) (Fs, error) {
+	return nil, errors.New("Google Cloud Storage disabled at build time")
+}
diff --git a/vfs/s3fs.go b/vfs/s3fs.go
index 63cf2bb1..be9467f3 100644
--- a/vfs/s3fs.go
+++ b/vfs/s3fs.go
@@ -1,3 +1,5 @@
+// +build !nos3
+
 package vfs
 
 import (
@@ -22,33 +24,6 @@ import (
 	"github.com/drakkan/sftpgo/utils"
 )
 
-// S3FsConfig defines the configuration for S3 based filesystem
-type S3FsConfig struct {
-	Bucket string `json:"bucket,omitempty"`
-	// KeyPrefix is similar to a chroot directory for local filesystem.
-	// If specified the SFTP user will only see objects that starts with
-	// this prefix and so you can restrict access to a specific virtual
-	// folder. The prefix, if not empty, must not start with "/" and must
-	// end with "/".
-	// If empty the whole bucket contents will be available
-	KeyPrefix    string `json:"key_prefix,omitempty"`
-	Region       string `json:"region,omitempty"`
-	AccessKey    string `json:"access_key,omitempty"`
-	AccessSecret string `json:"access_secret,omitempty"`
-	Endpoint     string `json:"endpoint,omitempty"`
-	StorageClass string `json:"storage_class,omitempty"`
-	// The buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB,
-	// and if this value is set to zero, the default value (5MB) for the AWS SDK will be used.
-	// The minimum allowed value is 5.
-	// Please note that if the upload bandwidth between the SFTP client and SFTPGo is greater than
-	// the upload bandwidth between SFTPGo and S3 then the SFTP client have to wait for the upload
-	// of the last parts to S3 after it ends the file upload to SFTPGo, and it may time out.
-	// Keep this in mind if you customize these parameters.
-	UploadPartSize int64 `json:"upload_part_size,omitempty"`
-	// How many parts are uploaded in parallel
-	UploadConcurrency int `json:"upload_concurrency,omitempty"`
-}
-
 // S3Fs is a Fs implementation for Amazon S3 compatible object storage.
 type S3Fs struct {
 	connectionID   string
@@ -59,6 +34,10 @@ type S3Fs struct {
 	ctxLongTimeout time.Duration
 }
 
+func init() {
+	utils.AddFeature("+s3")
+}
+
 // NewS3Fs returns an S3Fs object that allows to interact with an s3 compatible
 // object storage
 func NewS3Fs(connectionID, localTempDir string, config S3FsConfig) (Fs, error) {
diff --git a/vfs/s3fs_disabled.go b/vfs/s3fs_disabled.go
new file mode 100644
index 00000000..2e39d39b
--- /dev/null
+++ b/vfs/s3fs_disabled.go
@@ -0,0 +1,18 @@
+// +build nos3
+
+package vfs
+
+import (
+	"errors"
+
+	"github.com/drakkan/sftpgo/utils"
+)
+
+func init() {
+	utils.AddFeature("-s3")
+}
+
+// NewS3Fs returns an error, S3 is disabled
+func NewS3Fs(connectionID, localTempDir string, config S3FsConfig) (Fs, error) {
+	return nil, errors.New("S3 disabled at build time")
+}
diff --git a/vfs/vfs.go b/vfs/vfs.go
index 96a76b02..7432101c 100644
--- a/vfs/vfs.go
+++ b/vfs/vfs.go
@@ -44,6 +44,49 @@ type Fs interface {
 	Join(elem ...string) string
 }
 
+// S3FsConfig defines the configuration for S3 based filesystem
+type S3FsConfig struct {
+	Bucket string `json:"bucket,omitempty"`
+	// KeyPrefix is similar to a chroot directory for local filesystem.
+	// If specified the SFTP user will only see objects that starts with
+	// this prefix and so you can restrict access to a specific virtual
+	// folder. The prefix, if not empty, must not start with "/" and must
+	// end with "/".
+	// If empty the whole bucket contents will be available
+	KeyPrefix    string `json:"key_prefix,omitempty"`
+	Region       string `json:"region,omitempty"`
+	AccessKey    string `json:"access_key,omitempty"`
+	AccessSecret string `json:"access_secret,omitempty"`
+	Endpoint     string `json:"endpoint,omitempty"`
+	StorageClass string `json:"storage_class,omitempty"`
+	// The buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB,
+	// and if this value is set to zero, the default value (5MB) for the AWS SDK will be used.
+	// The minimum allowed value is 5.
+	// Please note that if the upload bandwidth between the SFTP client and SFTPGo is greater than
+	// the upload bandwidth between SFTPGo and S3 then the SFTP client have to wait for the upload
+	// of the last parts to S3 after it ends the file upload to SFTPGo, and it may time out.
+	// Keep this in mind if you customize these parameters.
+	UploadPartSize int64 `json:"upload_part_size,omitempty"`
+	// How many parts are uploaded in parallel
+	UploadConcurrency int `json:"upload_concurrency,omitempty"`
+}
+
+// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
+type GCSFsConfig struct {
+	Bucket string `json:"bucket,omitempty"`
+	// KeyPrefix is similar to a chroot directory for local filesystem.
+	// If specified the SFTP user will only see objects that starts with
+	// this prefix and so you can restrict access to a specific virtual
+	// folder. The prefix, if not empty, must not start with "/" and must
+	// end with "/".
+	// If empty the whole bucket contents will be available
+	KeyPrefix            string `json:"key_prefix,omitempty"`
+	CredentialFile       string `json:"-"`
+	Credentials          string `json:"credentials,omitempty"`
+	AutomaticCredentials int    `json:"automatic_credentials,omitempty"`
+	StorageClass         string `json:"storage_class,omitempty"`
+}
+
 // PipeWriter defines a wrapper for pipeat.PipeWriterAt.
 type PipeWriter struct {
 	writer *pipeat.PipeWriterAt