<commit_before>/*
 * SessionFiles.cpp
 *
 * Copyright (C) 2009-11 by RStudio, Inc.
 *
 * 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 "SessionFiles.hpp"

#include <csignal>

#include <vector>
#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>

#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>

#include <core/Error.hpp>
#include <core/Log.hpp>
#include <core/FileSerializer.hpp>
#include <core/FilePath.hpp>
#include <core/FileInfo.hpp>
#include <core/Settings.hpp>
#include <core/Exec.hpp>
#include <core/DateTime.hpp>

#include <core/system/DirectoryMonitor.hpp>

#include <core/http/Util.hpp>
#include <core/http/Request.hpp>
#include <core/http/Response.hpp>

#include <core/json/Json.hpp>

#include <r/RSexp.hpp>
#include <r/RExec.hpp>
#include <r/RRoutines.hpp>
#include <r/RErrorCategory.hpp>

#include <session/SessionClientEvent.hpp>
#include <session/SessionModuleContext.hpp>
#include <session/SessionOptions.hpp>
#include "SessionFilesQuotas.hpp"
#include "SessionSourceControl.hpp"

using namespace core ;

namespace session {

namespace modules { 
namespace files {

namespace {

// record previously monitored path for restoration after pause
std::string s_pausedDirectoryMonitorPath;

// constants for enquing file changed events
const char * const kType = "type";
const char * const kFile = "file";
const char * const kTargetFile = "targetFile";
   
void enqueFileChangeEvent(const source_control::StatusResult& statusResult,
                          const core::system::FileChangeEvent& event)
{
   using namespace source_control;

   json::Object fileChange ;
   fileChange[kType] = event.type();

   FilePath filePath(event.fileInfo().absolutePath());

   json::Object fileSystemItem = module_context::createFileSystemItem(event.fileInfo());
   fileSystemItem["vcs_status"] = statusResult.getStatus(filePath).status();
   fileChange[kFile] = fileSystemItem;


   ClientEvent clientEvent(client_events::kFileChanged, fileChange);
   module_context::enqueClientEvent(clientEvent);
}
   
// NOTE: we explicitly fire removed events for directories because of
// limitations in the way we use inotify in DirectoryMonitor. once we 
// lift these restrictions we need to get rid of these explicit calls
void enqueFileRemovedEvent(const FileInfo& fileInfo)
{
   using core::system::FileChangeEvent;
   enqueFileChangeEvent(source_control::StatusResult(),
                        FileChangeEvent(FileChangeEvent::FileRemoved,
                                        fileInfo));
}

bool isVisible(const FilePath& file)
{
   // check extension for special file types which are always visible
   std::string ext = file.extensionLowerCase();
   if (ext == ".rprofile" ||
       ext == ".rdata"    ||
       ext == ".rhistory" ||
       ext == ".renviron" )
   {
      return true;
   }
   else
   {
      return !file.isHidden();
   }
}

// directory monitor instance. the DirectoryMonitor is started when a 
// call to list_files includes monitor=true or when a session which was
// previously monitoring files is resumed
core::system::DirectoryMonitor s_directoryMonitor;
 
bool fileEventFilter(const FileInfo& fileInfo)
{
   return isVisible(FilePath(fileInfo.absolutePath()));
}
   
void startMonitoring(const std::string& path)
{
   Error error = s_directoryMonitor.start(path,
                                          boost::bind(fileEventFilter, 
                                                         _1));
   if (error)
      LOG_ERROR(error);
}

// make sure that monitoring persists accross suspended sessions
const char * const kMonitoredPath = "files.monitored-path";   

void onSuspend(Settings* pSettings)
{
   // get monitored path and alias it
   std::string monitoredPath = s_directoryMonitor.path();
   if (!monitoredPath.empty())
   {
      monitoredPath = FilePath::createAliasedPath(
                                            FilePath(monitoredPath),
                                            module_context::userHomePath());
   }

   // set it
   pSettings->set(kMonitoredPath, monitoredPath);
}

void onResume(const Settings& settings)
{
   // get the monitored path
   std::string monitoredPath = settings.get(kMonitoredPath);
   if (!monitoredPath.empty())
   {
      // resolve aliases
      FilePath resolvedPath = FilePath::resolveAliasedPath(
                                            monitoredPath,
                                            module_context::userHomePath());

      // start monitoriing
      startMonitoring(resolvedPath.absolutePath());
   }

   quotas::checkQuotaStatus();
}
   
void onClientInit()
{
   quotas::checkQuotaStatus();
}
   
void onDetectChanges(module_context::ChangeSource source)
{
   // poll for events
   std::vector<core::system::FileChangeEvent> events;
   Error error = s_directoryMonitor.checkForEvents(&events);
   if (error)
      LOG_ERROR(error);

   if (events.empty())
      return;

   source_control::StatusResult statusResult;
   error = source_control::status(
         FilePath(events.front().fileInfo().absolutePath()).parent(),
         &statusResult);
   if (error)
      LOG_ERROR(error);

   // fire client events as necessary
   std::for_each(events.begin(), events.end(), boost::bind(enqueFileChangeEvent,
                                                           statusResult,
                                                           _1));
}
   
void onShutdown(bool terminatedNormally)
{
   Error error = s_directoryMonitor.stop();
   if (error)
      LOG_ERROR(error);
}


// extract a set of FilePath object from a list of home path relative strings
Error extractFilePaths(const json::Array& files, 
                       std::vector<FilePath>* pFilePaths)
{   
   for(json::Array::const_iterator 
         it = files.begin(); 
         it != files.end();
         ++it)
   {
      if (it->type() != json::StringType)
         return Error(json::errc::ParamTypeMismatch, ERROR_LOCATION);

      std::string file = it->get_str() ;
      pFilePaths->push_back(module_context::resolveAliasedPath(file)) ;
   }

   return Success() ;
}

   
// IN: String path
// OUT: Array<FileEntry> files
core::Error listFiles(const json::JsonRpcRequest& request, 
                      json::JsonRpcResponse* pResponse)
{
   // get path
   std::string path;
   bool monitor;
   Error error = json::readParams(request.params, &path, &monitor);
   if (error)
      return error ;
   
   // retreive list of files
   FilePath targetPath = module_context::resolveAliasedPath(path) ;
   std::vector<FilePath> files ;
   error = targetPath.children(&files) ;
   if (error)
      return error ;

   source_control::StatusResult vcsStatus;
   error = source_control::status(targetPath, &vcsStatus);
   if (error)
      return error;

   // sort the files by name
   std::sort(files.begin(), files.end(), compareAbsolutePathNoCase);
   
   // produce json listing
   json::Array jsonFiles ;
   BOOST_FOREACH( FilePath& filePath, files )
   {
      // files which may have been deleted after the listing or which
      // are not end-user visible
      if (filePath.exists() && isVisible(filePath))
      {
         source_control::VCSStatus status = vcsStatus.getStatus(filePath);
         json::Object fileObject = module_context::createFileSystemItem(filePath);
         fileObject["vcs_status"] = status.status();
         jsonFiles.push_back(fileObject) ;
      }
   }

   // return listing
   pResponse->setResult(jsonFiles) ;
   
   // setup monitoring if requested (merely log errors so if something
   // unexpected happens the user still gets the file listing)
   if (monitor)
      startMonitoring(targetPath.absolutePath());
   
   // success
   return Success();
}


// IN: String path
core::Error createFolder(const core::json::JsonRpcRequest& request,
                         json::JsonRpcResponse* pResponse)
{
   std::string path;
   Error error = json::readParam(request.params, 0, &path);
   if (error)
      return error ;   
   
   // create the directory
   FilePath folderPath = module_context::resolveAliasedPath(path) ;
   if (folderPath.exists())
   {
      return fileExistsError(ERROR_LOCATION);
   }
   else
   {
      Error createError = folderPath.ensureDirectory() ;
      if (createError)
         return createError ;
   }

   return Success() ;
}

// IN: Array<String> paths
core::Error deleteFiles(const core::json::JsonRpcRequest& request,
                        json::JsonRpcResponse* pResponse)
{
   json::Array files;
   Error error = json::readParam(request.params, 0, &files);
   if (error)
      return error ;
   
   // extract vector of FilePath
   std::vector<FilePath> filePaths ;
   Error extractError = extractFilePaths(files, &filePaths) ;
   if (extractError)
      return extractError ;

   // delete each file
   Error deleteError ;
   for (std::vector<FilePath>::const_iterator 
         it = filePaths.begin();
         it != filePaths.end();
         ++it)
   {    
      // cache file info before we delete
      FileInfo fileInfo(*it);
     
      // remove the file
      deleteError = it->remove();
      if (deleteError)
         return deleteError ;
      
      // post delete event (inotify doesn't pick up folder deletes)
      if (fileInfo.isDirectory())
         enqueFileRemovedEvent(fileInfo);
   }

   return Success() ;
}
   
   
void copySourceFile(const FilePath& sourceDir, 
                    const FilePath& destDir,
                    int level,
                    const FilePath& sourceFilePath)
{
   // compute the target path
   std::string relativePath = sourceFilePath.relativePath(sourceDir);
   FilePath targetPath = destDir.complete(relativePath);
   
   // if the copy item is a directory just create it
   if (sourceFilePath.isDirectory())
   {
      Error error = targetPath.ensureDirectory();
      if (error)
         LOG_ERROR(error);
   }
   // otherwise copy it
   else
   {
      Error error = sourceFilePath.copy(targetPath);
      if (error)
         LOG_ERROR(error);
   }
}
   
// IN: String sourcePath, String targetPath
Error copyFile(const core::json::JsonRpcRequest& request,
               json::JsonRpcResponse* pResponse)
{
   // read params
   std::string sourcePath, targetPath;
   Error error = json::readParams(request.params, &sourcePath, &targetPath);
   if (error)
      return error ;
   
   // make sure the target path doesn't exist
   FilePath targetFilePath = module_context::resolveAliasedPath(targetPath);
   if (targetFilePath.exists())
      return fileExistsError(ERROR_LOCATION);

   // compute the source file path
   FilePath sourceFilePath = module_context::resolveAliasedPath(sourcePath);
   
   // copy directories recursively
   Error copyError ;
   if (sourceFilePath.isDirectory())
   {
      // create the target directory
      Error error = targetFilePath.ensureDirectory();
      if (error)
         return error ;
      
      // iterate over the source
      copyError = sourceFilePath.childrenRecursive(
        boost::bind(copySourceFile, sourceFilePath, targetFilePath, _1, _2));
   }
   else
   {
      copyError = sourceFilePath.copy(targetFilePath);
   }
   
   // check quota after copies
   quotas::checkQuotaStatus();
   
   // return error status
   return copyError;
}
      

// IN: Array<String> paths, String targetPath
Error moveFiles(const core::json::JsonRpcRequest& request,
                json::JsonRpcResponse* pResponse)
{
   json::Array files;
   std::string targetPath;
   Error error = json::readParams(request.params, &files, &targetPath);
   if (error)
      return error ;
   
   // extract vector of FilePath
   std::vector<FilePath> filePaths ;
   Error extractError = extractFilePaths(files, &filePaths) ;
   if (extractError)
      return extractError ;

   // create FilePath for target directory
   FilePath targetDirPath = module_context::resolveAliasedPath(targetPath);
   if (!targetDirPath.isDirectory())
      return Error(json::errc::ParamInvalid, ERROR_LOCATION);

   // move the files
   for (std::vector<FilePath>::const_iterator 
         it = filePaths.begin();
         it != filePaths.end();
         ++it)
   {
      // cache FileInfo before deleting
      FileInfo fileInfo(*it);
      
      // move the file
      FilePath targetPath = targetDirPath.childPath(it->filename()) ;
      Error moveError = it->move(targetPath) ;
      if (moveError)
         return moveError ;
      
      // enque delete event if necessary
      if (fileInfo.isDirectory())
         enqueFileRemovedEvent(fileInfo);
   }

   return Success() ;
}

// IN: String path, String targetPath
core::Error renameFile(const core::json::JsonRpcRequest& request,
                       json::JsonRpcResponse* pResponse)
{
   // read params
   std::string path, targetPath;
   Error error = json::readParams(request.params, &path, &targetPath);
   if (error)
      return error ;

   // if the destination already exists then send back file exists
    FilePath destPath = module_context::resolveAliasedPath(targetPath) ;
    if (destPath.exists())
       return fileExistsError(ERROR_LOCATION);
  
   // create file info now before we remove
   FilePath sourcePath = module_context::resolveAliasedPath(path);
   FileInfo sourceFileInfo(sourcePath);
      
   // move the file
   Error renameError = sourcePath.move(destPath);
   if (renameError)
      return renameError ;
                           
   // generate delete event for folders (inotify doesn't do this right now)
   if (sourceFileInfo.isDirectory())
      enqueFileRemovedEvent(sourceFileInfo);
   
   return Success() ;
}

void handleFilesRequest(const http::Request& request, 
                        http::Response* pResponse)
{   
   Options& options = session::options();
   if (options.programMode() != kSessionProgramModeServer)
   {
      pResponse->setError(http::status::NotFound,
                          request.uri() + " not found");
      return;
   }
   <commit_msg>change name of enqueFileRemovedEvent for contextual clarity<commit_after>/*
 * SessionFiles.cpp
 *
 * Copyright (C) 2009-11 by RStudio, Inc.
 *
 * 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 "SessionFiles.hpp"

#include <csignal>

#include <vector>
#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>

#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>

#include <core/Error.hpp>
#include <core/Log.hpp>
#include <core/FileSerializer.hpp>
#include <core/FilePath.hpp>
#include <core/FileInfo.hpp>
#include <core/Settings.hpp>
#include <core/Exec.hpp>
#include <core/DateTime.hpp>

#include <core/system/DirectoryMonitor.hpp>

#include <core/http/Util.hpp>
#include <core/http/Request.hpp>
#include <core/http/Response.hpp>

#include <core/json/Json.hpp>

#include <r/RSexp.hpp>
#include <r/RExec.hpp>
#include <r/RRoutines.hpp>
#include <r/RErrorCategory.hpp>

#include <session/SessionClientEvent.hpp>
#include <session/SessionModuleContext.hpp>
#include <session/SessionOptions.hpp>
#include "SessionFilesQuotas.hpp"
#include "SessionSourceControl.hpp"

using namespace core ;

namespace session {

namespace modules { 
namespace files {

namespace {

// record previously monitored path for restoration after pause
std::string s_pausedDirectoryMonitorPath;

// constants for enquing file changed events
const char * const kType = "type";
const char * const kFile = "file";
const char * const kTargetFile = "targetFile";
   
void enqueFileChangeEvent(const source_control::StatusResult& statusResult,
                          const core::system::FileChangeEvent& event)
{
   using namespace source_control;

   json::Object fileChange ;
   fileChange[kType] = event.type();

   FilePath filePath(event.fileInfo().absolutePath());

   json::Object fileSystemItem = module_context::createFileSystemItem(event.fileInfo());
   fileSystemItem["vcs_status"] = statusResult.getStatus(filePath).status();
   fileChange[kFile] = fileSystemItem;


   ClientEvent clientEvent(client_events::kFileChanged, fileChange);
   module_context::enqueClientEvent(clientEvent);
}
   
// NOTE: we explicitly fire removed events for directories because of
// limitations in the way we use inotify in DirectoryMonitor. once we 
// lift these restrictions we need to get rid of these explicit calls
void enqueDirectoryRemovedEvent(const FileInfo& fileInfo)
{
   using core::system::FileChangeEvent;
   enqueFileChangeEvent(source_control::StatusResult(),
                        FileChangeEvent(FileChangeEvent::FileRemoved,
                                        fileInfo));
}

bool isVisible(const FilePath& file)
{
   // check extension for special file types which are always visible
   std::string ext = file.extensionLowerCase();
   if (ext == ".rprofile" ||
       ext == ".rdata"    ||
       ext == ".rhistory" ||
       ext == ".renviron" )
   {
      return true;
   }
   else
   {
      return !file.isHidden();
   }
}

// directory monitor instance. the DirectoryMonitor is started when a 
// call to list_files includes monitor=true or when a session which was
// previously monitoring files is resumed
core::system::DirectoryMonitor s_directoryMonitor;
 
bool fileEventFilter(const FileInfo& fileInfo)
{
   return isVisible(FilePath(fileInfo.absolutePath()));
}
   
void startMonitoring(const std::string& path)
{
   Error error = s_directoryMonitor.start(path,
                                          boost::bind(fileEventFilter, 
                                                         _1));
   if (error)
      LOG_ERROR(error);
}

// make sure that monitoring persists accross suspended sessions
const char * const kMonitoredPath = "files.monitored-path";   

void onSuspend(Settings* pSettings)
{
   // get monitored path and alias it
   std::string monitoredPath = s_directoryMonitor.path();
   if (!monitoredPath.empty())
   {
      monitoredPath = FilePath::createAliasedPath(
                                            FilePath(monitoredPath),
                                            module_context::userHomePath());
   }

   // set it
   pSettings->set(kMonitoredPath, monitoredPath);
}

void onResume(const Settings& settings)
{
   // get the monitored path
   std::string monitoredPath = settings.get(kMonitoredPath);
   if (!monitoredPath.empty())
   {
      // resolve aliases
      FilePath resolvedPath = FilePath::resolveAliasedPath(
                                            monitoredPath,
                                            module_context::userHomePath());

      // start monitoriing
      startMonitoring(resolvedPath.absolutePath());
   }

   quotas::checkQuotaStatus();
}
   
void onClientInit()
{
   quotas::checkQuotaStatus();
}
   
void onDetectChanges(module_context::ChangeSource source)
{
   // poll for events
   std::vector<core::system::FileChangeEvent> events;
   Error error = s_directoryMonitor.checkForEvents(&events);
   if (error)
      LOG_ERROR(error);

   if (events.empty())
      return;

   source_control::StatusResult statusResult;
   error = source_control::status(
         FilePath(events.front().fileInfo().absolutePath()).parent(),
         &statusResult);
   if (error)
      LOG_ERROR(error);

   // fire client events as necessary
   std::for_each(events.begin(), events.end(), boost::bind(enqueFileChangeEvent,
                                                           statusResult,
                                                           _1));
}
   
void onShutdown(bool terminatedNormally)
{
   Error error = s_directoryMonitor.stop();
   if (error)
      LOG_ERROR(error);
}


// extract a set of FilePath object from a list of home path relative strings
Error extractFilePaths(const json::Array& files, 
                       std::vector<FilePath>* pFilePaths)
{   
   for(json::Array::const_iterator 
         it = files.begin(); 
         it != files.end();
         ++it)
   {
      if (it->type() != json::StringType)
         return Error(json::errc::ParamTypeMismatch, ERROR_LOCATION);

      std::string file = it->get_str() ;
      pFilePaths->push_back(module_context::resolveAliasedPath(file)) ;
   }

   return Success() ;
}

   
// IN: String path
// OUT: Array<FileEntry> files
core::Error listFiles(const json::JsonRpcRequest& request, 
                      json::JsonRpcResponse* pResponse)
{
   // get path
   std::string path;
   bool monitor;
   Error error = json::readParams(request.params, &path, &monitor);
   if (error)
      return error ;
   
   // retreive list of files
   FilePath targetPath = module_context::resolveAliasedPath(path) ;
   std::vector<FilePath> files ;
   error = targetPath.children(&files) ;
   if (error)
      return error ;

   source_control::StatusResult vcsStatus;
   error = source_control::status(targetPath, &vcsStatus);
   if (error)
      return error;

   // sort the files by name
   std::sort(files.begin(), files.end(), compareAbsolutePathNoCase);
   
   // produce json listing
   json::Array jsonFiles ;
   BOOST_FOREACH( FilePath& filePath, files )
   {
      // files which may have been deleted after the listing or which
      // are not end-user visible
      if (filePath.exists() && isVisible(filePath))
      {
         source_control::VCSStatus status = vcsStatus.getStatus(filePath);
         json::Object fileObject = module_context::createFileSystemItem(filePath);
         fileObject["vcs_status"] = status.status();
         jsonFiles.push_back(fileObject) ;
      }
   }

   // return listing
   pResponse->setResult(jsonFiles) ;
   
   // setup monitoring if requested (merely log errors so if something
   // unexpected happens the user still gets the file listing)
   if (monitor)
      startMonitoring(targetPath.absolutePath());
   
   // success
   return Success();
}


// IN: String path
core::Error createFolder(const core::json::JsonRpcRequest& request,
                         json::JsonRpcResponse* pResponse)
{
   std::string path;
   Error error = json::readParam(request.params, 0, &path);
   if (error)
      return error ;   
   
   // create the directory
   FilePath folderPath = module_context::resolveAliasedPath(path) ;
   if (folderPath.exists())
   {
      return fileExistsError(ERROR_LOCATION);
   }
   else
   {
      Error createError = folderPath.ensureDirectory() ;
      if (createError)
         return createError ;
   }

   return Success() ;
}

// IN: Array<String> paths
core::Error deleteFiles(const core::json::JsonRpcRequest& request,
                        json::JsonRpcResponse* pResponse)
{
   json::Array files;
   Error error = json::readParam(request.params, 0, &files);
   if (error)
      return error ;
   
   // extract vector of FilePath
   std::vector<FilePath> filePaths ;
   Error extractError = extractFilePaths(files, &filePaths) ;
   if (extractError)
      return extractError ;

   // delete each file
   Error deleteError ;
   for (std::vector<FilePath>::const_iterator 
         it = filePaths.begin();
         it != filePaths.end();
         ++it)
   {    
      // cache file info before we delete
      FileInfo fileInfo(*it);
     
      // remove the file
      deleteError = it->remove();
      if (deleteError)
         return deleteError ;
      
      // post delete event (inotify doesn't pick up folder deletes)
      if (fileInfo.isDirectory())
         enqueDirectoryRemovedEvent(fileInfo);
   }

   return Success() ;
}
   
   
void copySourceFile(const FilePath& sourceDir, 
                    const FilePath& destDir,
                    int level,
                    const FilePath& sourceFilePath)
{
   // compute the target path
   std::string relativePath = sourceFilePath.relativePath(sourceDir);
   FilePath targetPath = destDir.complete(relativePath);
   
   // if the copy item is a directory just create it
   if (sourceFilePath.isDirectory())
   {
      Error error = targetPath.ensureDirectory();
      if (error)
         LOG_ERROR(error);
   }
   // otherwise copy it
   else
   {
      Error error = sourceFilePath.copy(targetPath);
      if (error)
         LOG_ERROR(error);
   }
}
   
// IN: String sourcePath, String targetPath
Error copyFile(const core::json::JsonRpcRequest& request,
               json::JsonRpcResponse* pResponse)
{
   // read params
   std::string sourcePath, targetPath;
   Error error = json::readParams(request.params, &sourcePath, &targetPath);
   if (error)
      return error ;
   
   // make sure the target path doesn't exist
   FilePath targetFilePath = module_context::resolveAliasedPath(targetPath);
   if (targetFilePath.exists())
      return fileExistsError(ERROR_LOCATION);

   // compute the source file path
   FilePath sourceFilePath = module_context::resolveAliasedPath(sourcePath);
   
   // copy directories recursively
   Error copyError ;
   if (sourceFilePath.isDirectory())
   {
      // create the target directory
      Error error = targetFilePath.ensureDirectory();
      if (error)
         return error ;
      
      // iterate over the source
      copyError = sourceFilePath.childrenRecursive(
        boost::bind(copySourceFile, sourceFilePath, targetFilePath, _1, _2));
   }
   else
   {
      copyError = sourceFilePath.copy(targetFilePath);
   }
   
   // check quota after copies
   quotas::checkQuotaStatus();
   
   // return error status
   return copyError;
}
      

// IN: Array<String> paths, String targetPath
Error moveFiles(const core::json::JsonRpcRequest& request,
                json::JsonRpcResponse* pResponse)
{
   json::Array files;
   std::string targetPath;
   Error error = json::readParams(request.params, &files, &targetPath);
   if (error)
      return error ;
   
   // extract vector of FilePath
   std::vector<FilePath> filePaths ;
   Error extractError = extractFilePaths(files, &filePaths) ;
   if (extractError)
      return extractError ;

   // create FilePath for target directory
   FilePath targetDirPath = module_context::resolveAliasedPath(targetPath);
   if (!targetDirPath.isDirectory())
      return Error(json::errc::ParamInvalid, ERROR_LOCATION);

   // move the files
   for (std::vector<FilePath>::const_iterator 
         it = filePaths.begin();
         it != filePaths.end();
         ++it)
   {
      // cache FileInfo before deleting
      FileInfo fileInfo(*it);
      
      // move the file
      FilePath targetPath = targetDirPath.childPath(it->filename()) ;
      Error moveError = it->move(targetPath) ;
      if (moveError)
         return moveError ;
      
      // enque delete event if necessary
      if (fileInfo.isDirectory())
         enqueDirectoryRemovedEvent(fileInfo);
   }

   return Success() ;
}

// IN: String path, String targetPath
core::Error renameFile(const core::json::JsonRpcRequest& request,
                       json::JsonRpcResponse* pResponse)
{
   // read params
   std::string path, targetPath;
   Error error = json::readParams(request.params, &path, &targetPath);
   if (error)
      return error ;

   // if the destination already exists then send back file exists
    FilePath destPath = module_context::resolveAliasedPath(targetPath) ;
    if (destPath.exists())
       return fileExistsError(ERROR_LOCATION);
  
   // create file info now before we remove
   FilePath sourcePath = module_context::resolveAliasedPath(path);
   FileInfo sourceFileInfo(sourcePath);
      
   // move the file
   Error renameError = sourcePath.move(destPath);
   if (renameError)
      return renameError ;
                           
   // generate delete event for folders (inotify doesn't do this right now)
   if (sourceFileInfo.isDirectory())
      enqueDirectoryRemovedEvent(sourceFileInfo);
   
   return Success() ;
}

void handleFilesRequest(const http::Request& request, 
                        http::Response* pResponse)
{   
   Options& options = session::options();
   if (options.programMode() != kSessionProgramModeServer)
   {
      pResponse->setError(http::status::NotFound,
                          request.uri() + " not found");
      return;
   }
   
{'fraction_non_alphanumeric': 0.10444630872483221, 'fraction_numerical': 0.0010486577181208054, 'mean_word_length': 2.8313914557385833, 'pattern_counts': {'":': 0, '<': 79, '<?xml version=': 0, '>': 97, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 34, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4726746', 'n_tokens_mistral': 8065, 'n_tokens_neox': 7833, 'n_words': 2038}