1# --------------------------------------------------------------------------------------------
2# Copyright (c) Microsoft Corporation. All rights reserved.
3# Licensed under the MIT License. See License.txt in the project root for license information.
4# --------------------------------------------------------------------------------------------
5
6import os
7import sys
8import zipfile
9import requests
10
11from azure.cli.command_modules.botservice.constants import CSHARP, JAVASCRIPT
12from knack.util import CLIError
13
14
15class BotPublishPrep:
16
17    @staticmethod
18    def create_upload_zip(logger, code_dir, include_node_modules=True):
19        file_excludes = ['upload.zip', 'db.lock', '.env']
20        folder_excludes = ['packages', 'bin', 'obj']
21
22        logger.info('Creating upload zip file, code directory %s.', code_dir)
23
24        if not include_node_modules:
25
26            logger.info('Adding node_modules to folders to exclude from zip file.')
27            folder_excludes.append('node_modules')
28
29        zip_filepath = os.path.abspath('upload.zip')
30        logger.info('Compressing bot source into %s.', zip_filepath)
31
32        save_cwd = os.getcwd()
33        os.chdir(code_dir)
34        try:
35            with zipfile.ZipFile(zip_filepath, 'w',
36                                 compression=zipfile.ZIP_DEFLATED) as zf:
37                path = os.path.normpath(os.curdir)
38                for dirpath, dirnames, filenames in os.walk(os.curdir, topdown=True):
39                    for item in folder_excludes:
40                        if item in dirnames:
41                            dirnames.remove(item)
42                    for name in sorted(dirnames):
43                        path = os.path.normpath(os.path.join(dirpath, name))
44                        zf.write(path, path)
45                    for name in filenames:
46                        if name in file_excludes:
47                            continue
48                        path = os.path.normpath(os.path.join(dirpath, name))
49                        if os.path.isfile(path):
50                            zf.write(path, path)
51        finally:
52            os.chdir(save_cwd)
53        return zip_filepath
54
55    @staticmethod
56    def prepare_publish_v4(logger, code_dir, proj_file, iis_info):
57        if iis_info['lang'] == JAVASCRIPT and iis_info['has_web_config'] and iis_info['has_iisnode_yml']:
58            logger.info("Necessary files for a Node.js deployment on IIS have been detected in the bot's folder. Not "
59                        "retrieving required files from Azure.")
60            return
61
62        save_cwd = os.getcwd()
63        os.chdir(code_dir)
64
65        logger.info('Preparing Bot Builder SDK v4 bot for publish, with code directory %s and project file %s.',
66                    code_dir, proj_file or '')
67
68        try:
69            if iis_info['lang'] == CSHARP:
70                logger.info('Detected bot language C#, Bot Builder version v4.')
71
72                if proj_file is None:
73                    raise CLIError('Expected --proj-file-path argument provided with the full path to the '
74                                   'bot csproj file for csharp bot with Bot Builder SDK v4 project.')
75                with open('.deployment', 'w') as f:
76                    f.write('[config]\n')
77                    proj_file = proj_file.lower()
78                    proj_file = proj_file if proj_file.endswith('.csproj') else proj_file + '.csproj'
79                    f.write('SCM_SCRIPT_GENERATOR_ARGS=--aspNetCore {0}\n'
80                            .format(BotPublishPrep.__find_proj(proj_file)))
81
82            elif iis_info['lang'] == JAVASCRIPT:
83                logger.info('Detected bot language JavaScript, Bot Builder version v4. Fetching necessary deployment '
84                            'files for deploying on IIS.')
85
86                # Retrieve iisnode.yml and web.config and hold in memory, then extract required missing files.
87                node_iis_zip = BotPublishPrep.__retrieve_node_v4_publish_zip()
88                BotPublishPrep.__extract_specific_file_from_zip(logger,
89                                                                node_iis_zip,
90                                                                iis_info['has_web_config'],
91                                                                iis_info['has_iisnode_yml'])
92
93        finally:
94            os.chdir(save_cwd)
95
96    @staticmethod
97    def __retrieve_node_v4_publish_zip():
98        """Retrieves required IIS Node.js v4 BotBuilder SDK deployment files from Azure.
99        :return: zipfile.ZipFile instance
100        """
101        response = requests.get('https://icscratch.blob.core.windows.net/bot-packages/node_v4_publish.zip')
102        if sys.version_info.major >= 3:
103            import io
104            return zipfile.ZipFile(io.BytesIO(response.content))
105        # If Python version is 2.X, use StringIO instead.
106        import StringIO  # pylint: disable=import-error
107        return zipfile.ZipFile(StringIO.StringIO(response.content))
108
109    @staticmethod
110    def __extract_specific_file_from_zip(logger, zip_file, web_config_exists, iisnode_yml_exists):
111        if not web_config_exists and not iisnode_yml_exists:
112            zip_file.extractall()
113            logger.info('"web.config" and "iisnode.yml" created in %s.' % os.getcwd())
114        elif not web_config_exists:
115            with open('web.config', 'wb') as w:
116                w.write(zip_file.read('web.config'))
117                logger.info('"web.config" created in %s.' % os.getcwd())
118        elif iisnode_yml_exists:
119            with open('iisnode.yml', 'wb') as i:
120                i.write(zip_file.read('iisnode.yml'))
121                logger.info('"iisnode.yml" created in %s.' % os.getcwd())
122
123    @staticmethod
124    def __find_proj(proj_file):
125        for root, _, files in os.walk(os.curdir):
126            for file_name in files:
127                if proj_file == file_name.lower():
128                    return os.path.relpath(os.path.join(root, file_name))
129        raise CLIError('project file not found. Please pass a valid --proj-file-path.')
130