old/pushPull.R

# Push/pull project, Jan 2018, H. Seltman
# 
# This is a teaching tool to encourage interaction in programming classes.
#
# It is based on the existance of an FTP server where files are shared and exchanged.
# This code can also be implemented as Python functions, SAS macros, etc.

# Educational use examples:
# 1) Students are working in class and a student wants to know why their code
#    failed or you want to show a working (or not working) example from a
#    particular student.  The student pushes the file with push("myFile.R")
#    and you pull it with pull("myFile.R", who="studentId").  Now you can
#    open the file in your current R session.
# 2) Students are attempting to code something, and you want everyone to
#    "catch up" to your version.  Run push("myfile.R").  Each student can
#    then run pull("myFile.R"), and open your version on their computer.

# Requirements:
# "RCurl" package
# An sftp account (ideally restricted to only sftp, with a filespace limit)

# Details:
#   Students use push(files) to upload file(s) from their computer to
#   the sftp site in their folder (as specified by their setup user name).
#   Typically one or more files in the current folder are specified, but
#   relative and absolute locations are allowed.
#
#   Students use pull(files) to download files to their current folder
#   from the root folder of the ftp site.
#
#   Teachers use push(files) to upload file(s) from their computer to the
#   sftp site's root folder (by specifying user name ".").
#
#   Teachers use pull(files, who="studentUserId") to download one or more
#   student files to their current folder.

# One-time setup usage (per computer user):
#   source("https://raw.githubusercontent.com/hseltman/pushPull/master/setup.R")
#
# One time setup (per R session): source("~/pushPull.R")
#
# As needed usage:
# push(files="myFile.R") copies 'files' from the user's computer to folder
#   "userName" in the ftp folder (where 'userName' comes from the config file).
# pull(files="myFile.R", who=NULL) copies 'files' from the main ftp folder
#   to the working directory of the user's computer, adding "-instructor"
#   before the extension.  This is the default "student" mode.
#   When the instructor is pulling from a specific ('who') student folder,
#   "-myStudent" is added to the file name before the file is stored in the
#   instructor's working directory.

# References:
# https://stackoverflow.com/questions/15950396/sftp-get-files-with-r
# https://jonkimanalyze.wordpress.com/2014/11/20/r-quick-sftp-file-transfer/

# Return value for all three functions: invisible NULL

# Upload function
push = function(files) {
  if (length(files) == 0 || !is.character(files))
    stop("'files' must be a string vector")

  # get sftp info from config (or tell user how to run setup)
  fname = path.expand(file.path("~", "pushPullConfig.csv"))
  if (!file.exists(fname) ||
      !is.data.frame((dtf=try(read.csv(fname, as.is=TRUE), silent=TRUE))) ||
      !all(c("key", "value") %in% names(dtf)) ||
      !all(c("sftpSite", "sftpName", "sftpPassword", "userName") %in% dtf$key)) {
    cat("Missing or malformed ~/pushPullConfig.csv\n")
    cat("Run:\n")
    cat("source('https://raw.githubusercontent.com/hseltman/pushPull/master/setup.R'')\n")
    cat("setup()")
    stop("try again after running setup")
  }
  sftpSite = dtf$value[dtf$key=="sftpSite"]
  sftpName = dtf$value[dtf$key=="sftpName"]
  sftpPassword = dtf$value[dtf$key=="sftpPassword"]
  userName = dtf$value[dtf$key=="userName"]

  # Check files
  fid = file.info(files)$isdir
  notFound = is.na(fid)
  if (any(notFound)) {
    stop("missing file(s): '", paste(files[notFound], collapse=", "))
  }
  if (any(fid)) {
    stop("folder not file: ", paste(files[fid], collapse=", "))
  }
  
  # Upload files
  if (!require(RCurl, quietly=TRUE, warn.conflicts=FALSE))
    stop("install 'RCurl' and try again")
  front = paste0("sftp://", sftpName, ":", sftpPassword, "@", sftpSite)
  opts = list(ftp.create.missing.dirs=TRUE)
  for (f in files) {
    fRemote = file.path(userName, basename(f))
    rtn = try(ftpUpload(f, file.path(front, fRemote), .opts=opts), silent=TRUE)
    if (is(rtn, "try-error")) {
      cat("Upload of", f, "failed.\n")
      cat("Message:", as.character(attr(rtn, "condition")))
    }
  }

  invisible(NULL)
}


# Download function
pull = function(files, who=NULL) {
  if (length(files) == 0 || !is.character(files))
    stop("'files' must be a string vector")
  
  # get sftp info from config (or tell user how to run setup)
  fname = path.expand(file.path("~", "pushPullConfig.csv"))
  if (!file.exists(fname) ||
      !is.data.frame((dtf=try(read.csv(fname, as.is=TRUE), silent=TRUE))) ||
      !all(c("key", "value") %in% names(dtf)) ||
      !all(c("sftpSite", "sftpName", "sftpPassword", "userName") %in% dtf$key)) {
    cat("Missing or malformed ~/pushPullConfig.csv\n")
    cat("Run:\n")
    cat("source('https://raw.githubusercontent.com/hseltman/pushPull/master/setup.R'')\n")
    cat("setup()")
    stop("try again after running setup")
  }
  sftpSite = dtf$value[dtf$key=="sftpSite"]
  sftpName = dtf$value[dtf$key=="sftpName"]
  sftpPassword = dtf$value[dtf$key=="sftpPassword"]
  userName = dtf$value[dtf$key=="userName"]
  
  # Fixup files (subdirectories must be through "who")
  files = basename(files)
  if (!is.null(who)) {
    files = paste0(who, "/", files)
  }

  # Helper function to add "-user" to file names
  addUser = function(f, user) {
    if (is.null(user)) user = "."
    user = trimws(user)
    if (user %in% c("", ".")) user="instructor"
    f = strsplit(f, "[.]")[[1]]
    len = length(f)
    if (len == 1) return(paste0(f, "-", user))
    if (len > 2) {
      f = c(paste(f[1:(len-1)], collapse="."), f[len])
    }
    return(paste0(f[1], "-", user, ".", f[2]))
  }
  
  # Download files
  if (!require(RCurl, quietly=TRUE, warn.conflicts=FALSE))
    stop("install 'RCurl' and try again")
  front = paste0("sftp://", sftpName, ":", sftpPassword, "@", sftpSite)
  opts = list(ftp.create.missing.dirs=TRUE)
  for (f in files) {
    url = paste0("sftp://", sftpSite, "/", f)
    userpwd = paste0(sftpName, ":", sftpPassword)
    rtn = try(getURL(url, userpwd=userpwd), silent=TRUE)
    if (is(rtn, "try-error")) {
      cat("Download of", f, "failed.\n")
      cat("Message:", as.character(attr(rtn, "condition")))
    } else {
      fLocal = addUser(basename(f), who)
      rtn = try(write(rtn, fLocal), silent=TRUE)
      if (is(rtn, "try-error")) {
        cat("Download of", fLocal, "succeeded, but save to", getwd(), "failed.\n")
        cat("Message:", as.character(attr(rtn, "condition")))
      }
    }
  }
  
  invisible(NULL)
}
hseltman/pushPull documentation built on May 15, 2019, noon