<commit_before>/*
 * SessionRSConnect.cpp
 *
 * Copyright (C) 2009-14 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

#include "SessionRSConnect.hpp"

#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>

#include <core/Error.hpp>
#include <core/Exec.hpp>

#include <r/RSexp.hpp>
#include <r/RExec.hpp>
#include <r/RJson.hpp>

#include <session/SessionModuleContext.hpp>
#include <session/SessionAsyncRProcess.hpp>

#define kFinishedMarker "Deployment completed: "
#define kRSConnectFolder "rsconnect/"
#define kPackratFolder "packrat/"

#define kMaxDeploymentSize 104857600

using namespace rstudio::core;

namespace rstudio {
namespace session {
namespace modules { 
namespace rsconnect {

namespace {

// transforms a JSON array of file names into a single string. If 'quoted',
// then the input strings are quoted and comma-delimited; otherwise, file names
// are pipe-delimited.
std::string quotedFilesFromArray(json::Array array, bool quoted) 
{
   std::string joined;
   for (size_t i = 0; i < array.size(); i++) 
   {
      joined += (quoted ? "'" : "") + 
                string_utils::utf8ToSystem(array[i].get_str()) +
                (quoted ? "'" : "");
      if (i < array.size() - 1) 
         joined += (quoted ? ", " : "|");
   }
   return joined;
}

class RSConnectPublish : public async_r::AsyncRProcess
{
public:
   static boost::shared_ptr<RSConnectPublish> create(
         const std::string& dir,
         const json::Array& fileList, 
         const std::string& file, 
         const std::string& sourceDoc,
         const std::string& account,
         const std::string& server,
         const std::string& app,
         const json::Array& additionalFilesList,
         const json::Array& ignoredFilesList,
         bool asMultiple,
         bool asStatic)
   {
      boost::shared_ptr<RSConnectPublish> pDeploy(new RSConnectPublish(file));

      std::string cmd("{ options(repos = c(CRAN='" +
                       module_context::CRANReposURL() + "')); ");

      // join and quote incoming filenames to deploy
      std::string deployFiles = quotedFilesFromArray(fileList, true);
      std::string additionalFiles = quotedFilesFromArray(additionalFilesList,
            false);
      std::string ignoredFiles = quotedFilesFromArray(ignoredFilesList,
            false);

      // if an R Markdown document or HTML document is being deployed, mark it
      // as the primary file 
      std::string primaryDoc;
      if (!file.empty())
      {
         FilePath docFile = module_context::resolveAliasedPath(file);
         std::string extension = docFile.extensionLowerCase();
         if (extension == ".rmd" || extension == ".html") 
         {
            primaryDoc = string_utils::utf8ToSystem(file);
         }
      }

      // form the deploy command to hand off to the async deploy process
      cmd += "rsconnect::deployApp("
             "appDir = '" + string_utils::utf8ToSystem(dir) + "'," +
             (deployFiles.empty() ? "" : "appFiles = c(" + 
                deployFiles + "), ") +
             (primaryDoc.empty() ? "" : "appPrimaryDoc = '" + 
                primaryDoc + "', ") + 
             (sourceDoc.empty() ? "" : "appSourceDoc = '" + 
                sourceDoc + "', ") + 
             "account = '" + account + "',"
             "server = '" + server + "', "
             "appName = '" + app + "', "
             "launch.browser = function (url) { "
             "   message('" kFinishedMarker "', url) "
             "}, "
             "lint = FALSE,"
             "metadata = list(" 
             "   asMultiple = " + (asMultiple ? "TRUE" : "FALSE") + ", "
             "   asStatic = " + (asStatic ? "TRUE" : "FALSE") + 
                 (additionalFiles.empty() ? "" : ", additionalFiles = '" + 
                    additionalFiles + "'") + 
                 (ignoredFiles.empty() ? "" : ", ignoredFiles = '" + 
                    ignoredFiles + "'") + 
             "))}";

      pDeploy->start(cmd.c_str(), FilePath(), async_r::R_PROCESS_VANILLA);
      return pDeploy;
   }

private:
   RSConnectPublish(const std::string& file)
   {
      sourceFile_ = file;
   }

   void onStderr(const std::string& output)
   {
      onOutput(module_context::kCompileOutputNormal, output);
   }

   void onStdout(const std::string& output)
   {
      onOutput(module_context::kCompileOutputError, output);
   }

   void onOutput(int type, const std::string& output)
   {
      r::sexp::Protect protect;
      Error error;

      // look on each line of emitted output to see whether it contains the
      // finished marker
      std::vector<std::string> lines;
      boost::algorithm::split(lines, output,
                              boost::algorithm::is_any_of("\n\r"));
      int ncharMarker = sizeof(kFinishedMarker) - 1;
      BOOST_FOREACH(std::string& line, lines)
      {
         if (line.substr(0, ncharMarker) == kFinishedMarker)
         {
            deployedUrl_ = line.substr(ncharMarker, line.size() - ncharMarker);

            // check to see if a source file was specified; if so return a URL
            // with the source file appended
            if (!sourceFile_.empty() &&
                !boost::algorithm::iends_with(deployedUrl_, ".rmd") &&
                (string_utils::toLower(sourceFile_) != "index.rmd"))
            {
               // append / to the URL if it doesn't already have one
               if (deployedUrl_[deployedUrl_.length() - 1] != '/')
                  deployedUrl_.append("/");
               deployedUrl_.append(sourceFile_);
            }
         }
      }

      // emit the output to the client for display
      module_context::CompileOutput deployOutput(type, output);
      ClientEvent event(client_events::kRmdRSConnectDeploymentOutput,
                        module_context::compileOutputAsJson(deployOutput));
      module_context::enqueClientEvent(event);
   }

   void onCompleted(int exitStatus)
   {
      // when the process completes, emit the discovered URL, if any
      ClientEvent event(client_events::kRmdRSConnectDeploymentCompleted,
                        deployedUrl_);
      module_context::enqueClientEvent(event);
   }

   std::string deployedUrl_;
   std::string sourceFile_;<commit_msg>don't append Rmd file to URL (now handled elsewhere)<commit_after>/*
 * SessionRSConnect.cpp
 *
 * Copyright (C) 2009-14 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

#include "SessionRSConnect.hpp"

#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>

#include <core/Error.hpp>
#include <core/Exec.hpp>

#include <r/RSexp.hpp>
#include <r/RExec.hpp>
#include <r/RJson.hpp>

#include <session/SessionModuleContext.hpp>
#include <session/SessionAsyncRProcess.hpp>

#define kFinishedMarker "Deployment completed: "
#define kRSConnectFolder "rsconnect/"
#define kPackratFolder "packrat/"

#define kMaxDeploymentSize 104857600

using namespace rstudio::core;

namespace rstudio {
namespace session {
namespace modules { 
namespace rsconnect {

namespace {

// transforms a JSON array of file names into a single string. If 'quoted',
// then the input strings are quoted and comma-delimited; otherwise, file names
// are pipe-delimited.
std::string quotedFilesFromArray(json::Array array, bool quoted) 
{
   std::string joined;
   for (size_t i = 0; i < array.size(); i++) 
   {
      joined += (quoted ? "'" : "") + 
                string_utils::utf8ToSystem(array[i].get_str()) +
                (quoted ? "'" : "");
      if (i < array.size() - 1) 
         joined += (quoted ? ", " : "|");
   }
   return joined;
}

class RSConnectPublish : public async_r::AsyncRProcess
{
public:
   static boost::shared_ptr<RSConnectPublish> create(
         const std::string& dir,
         const json::Array& fileList, 
         const std::string& file, 
         const std::string& sourceDoc,
         const std::string& account,
         const std::string& server,
         const std::string& app,
         const json::Array& additionalFilesList,
         const json::Array& ignoredFilesList,
         bool asMultiple,
         bool asStatic)
   {
      boost::shared_ptr<RSConnectPublish> pDeploy(new RSConnectPublish(file));

      std::string cmd("{ options(repos = c(CRAN='" +
                       module_context::CRANReposURL() + "')); ");

      // join and quote incoming filenames to deploy
      std::string deployFiles = quotedFilesFromArray(fileList, true);
      std::string additionalFiles = quotedFilesFromArray(additionalFilesList,
            false);
      std::string ignoredFiles = quotedFilesFromArray(ignoredFilesList,
            false);

      // if an R Markdown document or HTML document is being deployed, mark it
      // as the primary file 
      std::string primaryDoc;
      if (!file.empty())
      {
         FilePath docFile = module_context::resolveAliasedPath(file);
         std::string extension = docFile.extensionLowerCase();
         if (extension == ".rmd" || extension == ".html") 
         {
            primaryDoc = string_utils::utf8ToSystem(file);
         }
      }

      // form the deploy command to hand off to the async deploy process
      cmd += "rsconnect::deployApp("
             "appDir = '" + string_utils::utf8ToSystem(dir) + "'," +
             (deployFiles.empty() ? "" : "appFiles = c(" + 
                deployFiles + "), ") +
             (primaryDoc.empty() ? "" : "appPrimaryDoc = '" + 
                primaryDoc + "', ") + 
             (sourceDoc.empty() ? "" : "appSourceDoc = '" + 
                sourceDoc + "', ") + 
             "account = '" + account + "',"
             "server = '" + server + "', "
             "appName = '" + app + "', "
             "launch.browser = function (url) { "
             "   message('" kFinishedMarker "', url) "
             "}, "
             "lint = FALSE,"
             "metadata = list(" 
             "   asMultiple = " + (asMultiple ? "TRUE" : "FALSE") + ", "
             "   asStatic = " + (asStatic ? "TRUE" : "FALSE") + 
                 (additionalFiles.empty() ? "" : ", additionalFiles = '" + 
                    additionalFiles + "'") + 
                 (ignoredFiles.empty() ? "" : ", ignoredFiles = '" + 
                    ignoredFiles + "'") + 
             "))}";

      pDeploy->start(cmd.c_str(), FilePath(), async_r::R_PROCESS_VANILLA);
      return pDeploy;
   }

private:
   RSConnectPublish(const std::string& file)
   {
      sourceFile_ = file;
   }

   void onStderr(const std::string& output)
   {
      onOutput(module_context::kCompileOutputNormal, output);
   }

   void onStdout(const std::string& output)
   {
      onOutput(module_context::kCompileOutputError, output);
   }

   void onOutput(int type, const std::string& output)
   {
      r::sexp::Protect protect;
      Error error;

      // look on each line of emitted output to see whether it contains the
      // finished marker
      std::vector<std::string> lines;
      boost::algorithm::split(lines, output,
                              boost::algorithm::is_any_of("\n\r"));
      int ncharMarker = sizeof(kFinishedMarker) - 1;
      BOOST_FOREACH(std::string& line, lines)
      {
         if (line.substr(0, ncharMarker) == kFinishedMarker)
            deployedUrl_ = line.substr(ncharMarker, line.size() - ncharMarker);
      }

      // emit the output to the client for display
      module_context::CompileOutput deployOutput(type, output);
      ClientEvent event(client_events::kRmdRSConnectDeploymentOutput,
                        module_context::compileOutputAsJson(deployOutput));
      module_context::enqueClientEvent(event);
   }

   void onCompleted(int exitStatus)
   {
      // when the process completes, emit the discovered URL, if any
      ClientEvent event(client_events::kRmdRSConnectDeploymentCompleted,
                        deployedUrl_);
      module_context::enqueClientEvent(event);
   }

   std::string deployedUrl_;
   std::string sourceFile_;
{'fraction_non_alphanumeric': 0.12280294231513744, 'fraction_numerical': 0.003948896631823461, 'mean_word_length': 2.4507079882447234, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2641060', 'n_tokens_mistral': 3769, 'n_tokens_neox': 3597, 'n_words': 917}