Repository: conan-io/conan

Issue:
[feature] Support for custom directory for conan_server
Currently conan_server only supports being deployed to a default location - `~/.conan_server/`. My suggestion is to let the user set a custom directory by adding command-line argument that will let the user set where the server config and data will be stored. As far as I see from the code it should be something that is rather easy to do, and I would like to open a PR to add the functionality.

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).



Discussion:
Hi @jenia90 

Sure, feel free to submit a PR for it, thanks for wanting to contribute. Just make sure that it is opt-in and it will not break existing users.
Hi @memsharded 

Of course, it should be an optional flag that users can specify when they launch the server. What do you think about adding an environment variable, say CONAN_SERVER_DIR, as an alternative way of specifying the directory?
> What do you think about adding an environment variable, say CONAN_SERVER_DIR, as an alternative way of specifying the directory?

Typically I would go for 1, possible the simplest or most demanded "main" implementation, then only add alternative ways based on user demand. In this case, actually I would probably go with just the env-var because it would be more symmetric to the client (CONAN_USER_HOME), so something like an env-var (CONAN_SERVER_HOME). But if you feel the cli arg is better for your case, go ahead for it.

Patch:
diff --git a/conans/conan_server.py b/conans/conan_server.py
--- a/conans/conan_server.py
+++ b/conans/conan_server.py
@@ -1,14 +1,18 @@
 import argparse
 
 from conans.server.launcher import ServerLauncher
+from conans.util.env_reader import get_env
 
 
 def run():
     parser = argparse.ArgumentParser(description='Launch the server')
     parser.add_argument('--migrate', default=False, action='store_true',
                         help='Run the pending migrations')
+    parser.add_argument('--server_dir', '-d', default=None,
+                        help='Specify where to store server config and data.')
     args = parser.parse_args()
-    launcher = ServerLauncher(force_migration=args.migrate)
+    launcher = ServerLauncher(force_migration=args.migrate,
+                              server_dir=args.server_dir or get_env("CONAN_SERVER_HOME"))
     launcher.launch()
 
 
diff --git a/conans/server/conf/__init__.py b/conans/server/conf/__init__.py
--- a/conans/server/conf/__init__.py
+++ b/conans/server/conf/__init__.py
@@ -28,13 +28,17 @@ class ConanServerConfigParser(ConfigParser):
     values from environment variables or from file.
     Environment variables have PRECEDENCE over file values
     """
-    def __init__(self, base_folder, environment=None):
+
+    def __init__(self, base_folder, environment=None, is_custom_path=False):
         environment = environment or os.environ
 
         ConfigParser.__init__(self)
         environment = environment or os.environ
         self.optionxform = str  # This line keeps the case of the key, important for users case
-        self.conan_folder = os.path.join(base_folder, '.conan_server')
+        if is_custom_path:
+            self.conan_folder = base_folder
+        else:
+            self.conan_folder = os.path.join(base_folder, '.conan_server')
         self.config_filename = os.path.join(self.conan_folder, 'server.conf')
         self._loaded = False
         self.env_config = {"updown_secret": get_env("CONAN_UPDOWN_SECRET", None, environment),
diff --git a/conans/server/launcher.py b/conans/server/launcher.py
--- a/conans/server/launcher.py
+++ b/conans/server/launcher.py
@@ -15,12 +15,17 @@
 
 
 class ServerLauncher(object):
-    def __init__(self, force_migration=False):
+    def __init__(self, force_migration=False, server_dir=None):
         self.force_migration = force_migration
-        user_folder = conan_expand_user("~")
-        server_folder = os.path.join(user_folder, '.conan_server')
+        if server_dir:
+            user_folder = server_folder = server_dir
+        else:
+            user_folder = conan_expand_user("~")
+            server_folder = os.path.join(user_folder, '.conan_server')
 
-        server_config = migrate_and_get_server_config(user_folder, self.force_migration)
+        server_config = migrate_and_get_server_config(
+            user_folder, self.force_migration, server_dir is not None
+        )
         custom_auth = server_config.custom_authenticator
         if custom_auth:
             authenticator = load_authentication_plugin(server_folder, custom_auth)
diff --git a/conans/server/migrate.py b/conans/server/migrate.py
--- a/conans/server/migrate.py
+++ b/conans/server/migrate.py
@@ -5,13 +5,13 @@
 from conans.util.log import logger
 
 
-def migrate_and_get_server_config(base_folder, force_migration=False):
-    server_config = ConanServerConfigParser(base_folder)
+def migrate_and_get_server_config(base_folder, force_migration=False, is_custom_path=False):
+    server_config = ConanServerConfigParser(base_folder, is_custom_path=is_custom_path)
     storage_path = server_config.disk_storage_path
     migrator = ServerMigrator(server_config.conan_folder, storage_path,
                               Version(SERVER_VERSION), logger, force_migration)
     migrator.migrate()
 
     # Init again server_config, migrator could change something
-    server_config = ConanServerConfigParser(base_folder)
+    server_config = ConanServerConfigParser(base_folder, is_custom_path=is_custom_path)
     return server_config
diff --git a/conans/server/server_launcher.py b/conans/server/server_launcher.py
--- a/conans/server/server_launcher.py
+++ b/conans/server/server_launcher.py
@@ -1,6 +1,8 @@
 from conans.server.launcher import ServerLauncher
 
-launcher = ServerLauncher()
+from conans.util.env_reader import get_env
+
+launcher = ServerLauncher(server_dir=get_env("CONAN_SERVER_HOME"))
 app = launcher.server.root_app
 
 


Test patch:
diff --git a/conans/test/unittests/server/conan_server_config_parser_test.py b/conans/test/unittests/server/conan_server_config_parser_test.py
--- a/conans/test/unittests/server/conan_server_config_parser_test.py
+++ b/conans/test/unittests/server/conan_server_config_parser_test.py
@@ -72,3 +72,36 @@ def test_relative_public_url(self):
 
         server_config = ConanServerConfigParser(tmp_dir)
         self.assertEqual(server_config.public_url, "v1")
+
+    def test_custom_server_folder_path(self):
+        tmp_dir = temp_folder()
+        server_dir = os.path.join(tmp_dir, ".custom_conan_server")
+        mkdir(server_dir)
+        conf_path = os.path.join(server_dir, "server.conf")
+        server_conf = """
+[server]
+
+[write_permissions]
+
+[users]
+        """
+        save(conf_path, server_conf)
+        server_config = ConanServerConfigParser(server_dir, is_custom_path=True)
+        self.assertEqual(server_config.conan_folder, server_dir)
+
+    def test_custom_server_path_has_custom_data_path(self):
+        tmp_dir = temp_folder()
+        server_dir = os.path.join(tmp_dir, ".custom_conan_server")
+        mkdir(server_dir)
+        conf_path = os.path.join(server_dir, "server.conf")
+        server_conf = """
+[server]
+disk_storage_path: ./custom_data
+
+[write_permissions]
+
+[users]
+        """
+        save(conf_path, server_conf)
+        server_config = ConanServerConfigParser(server_dir, is_custom_path=True)
+        self.assertEqual(server_config.disk_storage_path, os.path.join(server_dir, "custom_data"))