Skip to content
This repository has been archived by the owner on Jan 10, 2023. It is now read-only.

Commit

Permalink
Add -dry-run support to upload and download.
Browse files Browse the repository at this point in the history
These print the files that would be transferred, where they'd be stored
(and their sizes), without actually transferring anything.

Issue #101.
  • Loading branch information
Matt Pharr committed Jan 27, 2016
1 parent 7b350ea commit 7560452
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 52 deletions.
31 changes: 26 additions & 5 deletions download.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,23 @@ import (
)

func downloadUsage() {
fmt.Printf("Usage: skicka download [-ignore-times] [-download-google-apps-files] drive_path local_path\n")
fmt.Printf("Usage: skicka download [-ignore-times] [-dry-run] [-download-google-apps-files]\n")
fmt.Printf(" drive_path local_path\n")
fmt.Printf("Run \"skicka help\" for more detailed help text.\n")
}

func download(args []string) int {
var drivePath, localPath string
ignoreTimes := false
downloadGoogleAppsFiles := false
dryRun := false
for i := 0; i < len(args); i++ {
if args[i] == "-ignore-times" {
ignoreTimes = true
} else if args[i] == "-download-google-apps-files" {
downloadGoogleAppsFiles = true
} else if args[i] == "-dry-run" {
dryRun = true
} else if drivePath == "" {
drivePath = filepath.Clean(args[i])
} else if localPath == "" {
Expand Down Expand Up @@ -80,7 +84,8 @@ func download(args []string) int {
var errs int
if files[0].IsFolder() {
// Download a folder from Drive to the local system.
errs = syncHierarchyDown(drivePath, localPath, trustTimes, downloadGoogleAppsFiles)
errs = syncHierarchyDown(drivePath, localPath, trustTimes,
downloadGoogleAppsFiles, dryRun)
} else {
// Only download a single file.
stat, err := os.Stat(localPath)
Expand All @@ -93,7 +98,7 @@ func download(args []string) int {
if !downloadGoogleAppsFiles && files[0].IsGoogleAppsFile() {
message("%s: skipping Google Apps file.", files[0].Path)
} else {
err = syncOneFileDown(files[0], localPath, trustTimes)
err = syncOneFileDown(files[0], localPath, trustTimes, dryRun)
if err != nil {
fmt.Fprintf(os.Stderr, "skicka: %s: %s\n", drivePath, err)
errs++
Expand All @@ -107,14 +112,19 @@ func download(args []string) int {

// Synchronize a single file from Google Drive to the local file system at
// `localPath`.
func syncOneFileDown(file *gdrive.File, localPath string, trustTimes bool) error {
func syncOneFileDown(file *gdrive.File, localPath string, trustTimes bool,
dryRun bool) error {
needsDownload, err := fileNeedsDownload(localPath, file, trustTimes)
if err != nil {
return fmt.Errorf("%s: error determining if file needs "+
"download: %v\n", file.Path, err)
}

if needsDownload {
if dryRun {
fmt.Printf("%s -> %s (%d bytes)\n", file.Path, localPath, file.FileSize)
return nil
}
pb := getProgressBar(file.FileSize)
if pb != nil {
defer pb.Finish()
Expand All @@ -133,7 +143,7 @@ func syncOneFileDown(file *gdrive.File, localPath string, trustTimes bool) error

// Synchronize an entire folder hierarchy from Drive to a local directory.
func syncHierarchyDown(driveBasePath string, localBasePath string, trustTimes bool,
downloadGoogleAppsFiles bool) int {
downloadGoogleAppsFiles bool, dryRun bool) int {
// First, make sure the user isn't asking us to download a directory on
// top of a file.
if stat, err := os.Stat(localBasePath); err == nil && !stat.IsDir() {
Expand Down Expand Up @@ -177,6 +187,17 @@ func syncHierarchyDown(driveBasePath string, localBasePath string, trustTimes bo
// file.
localPathMap := createPathMap(uniqueDriveFiles, localBasePath, driveBasePath)

if dryRun {
var totalBytes int64
for _, f := range uniqueDriveFiles {
fmt.Printf("%s -> %s (%d bytes)\n", f.Path, localPathMap[f.Path],
f.FileSize)
totalBytes += f.FileSize
}
fmt.Printf("Total bytes %d\n", totalBytes)
return 0
}

// First create all of the local directories, so that the downloaded
// files have somewhere to land. For any already-existing directories,
// update their permissions to match the permissions of the
Expand Down
40 changes: 22 additions & 18 deletions skicka.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// skicka.go
// Copyright(c)2014-2015 Google, Inc.
// Copyright(c)2014-2016 Google, Inc.
//
// Tool for transferring files to/from Google Drive and related operations.
//
Expand Down Expand Up @@ -857,12 +857,12 @@ Commands and their options are:
ls List the files and directories in the given Google Drive folder.
Arguments: [-d, -l, -ll, -r] [drive_path ...],
where -l and -ll specify long (including sizes and update times)
and really long output (also including MD5 checksums), respectively.
The -r argument causes ls to recursively list all files in the
hierarchy rooted at the base directory, and -d causes directories
specified on the command line to be listed as files (i.e., their
contents aren't listed.)
where -l and -ll specify long (including sizes and update
times) and really long output (also including MD5 checksums),
respectively. The -r argument causes ls to recursively list
all files in the hierarchy rooted at the base directory, and
-d causes directories specified on the command line to be
listed as files (i.e., their contents aren't listed.)
mkdir Create a new directory (folder) at the given Google Drive path.
Arguments: [-p] drive_path ...,
Expand All @@ -871,23 +871,27 @@ Commands and their options are:
rm Remove a file or directory at the given Google Drive path.
Arguments: [-r, -s] drive_path ...,
where files and directories are recursively removed if -r is specified
and the google drive trash is skipped if -s is specified. The default
behavior is to fail if the drive path specified is a directory and -r is
not specified, and to send files to the trash instead of permanently
deleting them.
where files and directories are recursively removed if -r is
specified and the google drive trash is skipped if -s is
specified. The default behavior is to fail if the drive path
specified is a directory and -r is not specified, and to send
files to the trash instead of permanently deleting them.
upload Uploads all files in the local directory and its children to the
given Google Drive path. Skips files that have already been
uploaded.
Arguments: [-ignore-times] [-encrypt] [-follow-symlinks <maxdepth>] local_path drive_path
Arguments: [-ignore-times] [-encrypt] [-follow-symlinks <maxdepth>]
local_path drive_path
Options valid for both "upload" and "download":
-ignore-times Normally, skicka assumes that if the timestamp of a local file
matches the timestamp of the file on Drive and the files have
the same size, then it isn't necessary to confirm that the
file contents match. The -ignore-times flag can be used to
force checking file contents in this case.
-dry-run Don't actually upload or download, but print the paths of
all files that would be transferred.
-ignore-times Normally, skicka assumes that if the timestamp of a local
file matches the timestamp of the file on Drive and the
files have the same size, then it isn't necessary to
confirm that the file contents match. The -ignore-times
flag can be used to force checking file contents in this
case.
General options valid for all commands:
-config <filename> General skicka configuration file. Default: ~/.skicka.config.
Expand Down
84 changes: 55 additions & 29 deletions upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ import (
)

func uploadUsage() {
fmt.Printf("Usage: skicka upload [-ignore-times] [-encrypt] [-follow-symlinks <maxdepth>] local_path drive_path\n")
fmt.Printf("Usage: skicka upload [-ignore-times] [-encrypt] [-follow-symlinks <maxdepth>]\n")
fmt.Printf(" [-dry-run] local_path drive_path\n")
fmt.Printf("Run \"skicka help\" for more detailed help text.\n")
}

func upload(args []string) int {
ignoreTimes := false
encrypt := false
dryRun := false

if len(args) < 2 {
uploadUsage()
Expand All @@ -58,6 +60,8 @@ func upload(args []string) int {
ignoreTimes = true
case "-encrypt":
encrypt = true
case "-dry-run":
dryRun = true
case "-follow-symlinks":
var err error
maxSymlinkDepth, err = strconv.Atoi(args[i+1])
Expand Down Expand Up @@ -102,7 +106,7 @@ func upload(args []string) int {

syncStartTime = time.Now()
errs := syncHierarchyUp(localPath, drivePath, encrypt, trustTimes,
maxSymlinkDepth)
maxSymlinkDepth, dryRun)
printFinalStats()

return errs
Expand Down Expand Up @@ -280,18 +284,29 @@ func uploadFileContents(localPath string, driveFile *gdrive.File, encrypt bool,
// localPath is the file or directory to start with, driveRoot is
// the directory into which the file/directory will be sent
func syncHierarchyUp(localPath string, driveRoot string, encrypt bool, trustTimes bool,
maxSymlinkDepth int) int {
maxSymlinkDepth int, dryRun bool) int {
if encrypt && key == nil {
key = decryptEncryptionKey()
}

fileMappings, nUploadErrors := compileUploadFileTree(localPath, driveRoot,
encrypt, trustTimes, maxSymlinkDepth)
encrypt, trustTimes, maxSymlinkDepth, dryRun)
if len(fileMappings) == 0 {
message("No files to be uploaded.")
return 0
}

if dryRun {
var totalSize int64
for _, f := range fileMappings {
fmt.Printf("%s -> %s (%d bytes)\n", f.LocalPath, f.DrivePath,
f.LocalFileInfo.Size())
totalSize += f.LocalFileInfo.Size()
}
fmt.Printf("Total bytes %d\n", totalSize)
return 0
}

nBytesToUpload := int64(0)
for _, info := range fileMappings {
if !info.LocalFileInfo.IsDir() {
Expand Down Expand Up @@ -462,7 +477,7 @@ func isSymlink(stat os.FileInfo) bool {
// Starts with the efficient checks that may be able to let us quickly
// determine one way or the other before going to the more expensive ones.
func fileNeedsUpload(localPath, drivePath string, stat os.FileInfo,
encrypt, trustTimes bool) (bool, error) {
encrypt, trustTimes bool, dryRun bool) (bool, error) {
// Don't upload if the filename matches one of the regular expressions
// of files to ignore.
for _, re := range config.Upload.Ignored_Regexp {
Expand Down Expand Up @@ -515,26 +530,33 @@ func fileNeedsUpload(localPath, drivePath string, stat os.FileInfo,
localPath, drivePath)
}

// With that check out of the way, take the opportunity to make sure
// the file has all of the properties that we expect.
if err := createMissingProperties(driveFile, stat.Mode(), encrypt); err != nil {
debug.Printf("%s: error creating properties: %s", driveFile, err)
return false, err
}
// FIXME: a function named "fileNeedsUpload()" shouldn't be messing
// around with creating properties, upading modification times, etc.;
// all this should happen in a more appropriate place, so we don't need
// to plumb through and pay attention to whether we're doing a dry run
// here.
if !dryRun {
// With that check out of the way, take the opportunity to make sure
// the file has all of the properties that we expect.
if err := createMissingProperties(driveFile, stat.Mode(), encrypt); err != nil {
debug.Printf("%s: error creating properties: %s", driveFile, err)
return false, err
}

// Go ahead and update the file's permissions if they've changed.
bitsString := fmt.Sprintf("%#o", stat.Mode()&os.ModePerm)
debug.Printf("%s: updating permissions to %s", drivePath, bitsString)
if err := gd.UpdateProperty(driveFile, "Permissions", bitsString); err != nil {
debug.Printf("%s: error updating permissions properties: %s", driveFile, err)
return false, err
}
// Go ahead and update the file's permissions if they've changed.
bitsString := fmt.Sprintf("%#o", stat.Mode()&os.ModePerm)
debug.Printf("%s: updating permissions to %s", drivePath, bitsString)
if err := gd.UpdateProperty(driveFile, "Permissions", bitsString); err != nil {
debug.Printf("%s: error updating permissions properties: %s", driveFile, err)
return false, err
}

// If it's a directory, once it's created and the permissions and times
// are updated (if needed), we're all done.
if stat.IsDir() {
debug.Printf("%s: updating modification time to %s", drivePath, normalizeModTime(stat.ModTime()))
return false, gd.UpdateModificationTime(driveFile, normalizeModTime(stat.ModTime()))
// If it's a directory, once it's created and the permissions and times
// are updated (if needed), we're all done.
if stat.IsDir() {
debug.Printf("%s: updating modification time to %s", drivePath, normalizeModTime(stat.ModTime()))
return false, gd.UpdateModificationTime(driveFile, normalizeModTime(stat.ModTime()))
}
}

// Compare file sizes.
Expand Down Expand Up @@ -603,6 +625,9 @@ func fileNeedsUpload(localPath, drivePath string, stat os.FileInfo,
// The timestamp of the local file is different, but the checksums
// match, so just update the modified time on Drive and don't upload
// the file contents.
if dryRun {
return false, nil
}
debug.Printf("%s: updating modification time (#2) to %s", drivePath, localTime)
return false, gd.UpdateModificationTime(driveFile, localTime)
}
Expand Down Expand Up @@ -639,7 +664,7 @@ func resolveSymlinks(path string, stat os.FileInfo, maxSymlinkDepth *int) (strin
// encountered, determine if the file needs to be uploaded. If so, an entry
// is added to the returned localToRemoteFileMapping array.
func walkPathForUploads(localPath, drivePath string, encrypt,
trustTimes bool, maxSymlinkDepth int) ([]localToRemoteFileMapping, int32) {
trustTimes bool, maxSymlinkDepth int, dryRun bool) ([]localToRemoteFileMapping, int32) {
var fileMappings []localToRemoteFileMapping
nErrs := int32(0)

Expand Down Expand Up @@ -673,7 +698,7 @@ func walkPathForUploads(localPath, drivePath string, encrypt,
// the maxDepth passed in accounts for the number of links we
// followed to get to this point.
mappings, ne := walkPathForUploads(path, drivePath, encrypt,
trustTimes, maxDepth)
trustTimes, maxDepth, dryRun)
fileMappings = append(fileMappings, mappings...)
nErrs += ne
return nil
Expand All @@ -683,7 +708,8 @@ func walkPathForUploads(localPath, drivePath string, encrypt,
drivePath += encryptionSuffix
}

upload, err := fileNeedsUpload(path, drivePath, stat, encrypt, trustTimes)
upload, err := fileNeedsUpload(path, drivePath, stat, encrypt, trustTimes,
dryRun)
if err != nil {
fmt.Fprintf(os.Stderr, "skicka: %s\n", err)
nErrs++
Expand All @@ -706,7 +732,7 @@ func walkPathForUploads(localPath, drivePath string, encrypt,
}

func compileUploadFileTree(localPath, drivePath string,
encrypt, trustTimes bool, maxSymlinkDepth int) ([]localToRemoteFileMapping, int32) {
encrypt, trustTimes bool, maxSymlinkDepth int, dryRun bool) ([]localToRemoteFileMapping, int32) {
// Walk the local directory hierarchy starting at 'localPath' and build
// an array of files that may need to be synchronized.
nUploadErrors := int32(0)
Expand Down Expand Up @@ -745,7 +771,7 @@ func compileUploadFileTree(localPath, drivePath string,

var fileMappings []localToRemoteFileMapping
upload, err := fileNeedsUpload(localPath, drivePath, stat,
encrypt, trustTimes)
encrypt, trustTimes, dryRun)
if err != nil {
fmt.Fprintf(os.Stderr, "skicka: %s", err)
nUploadErrors++
Expand All @@ -758,7 +784,7 @@ func compileUploadFileTree(localPath, drivePath string,

message("Getting list of local files... ")
fileMappings, nErrs := walkPathForUploads(localPath, drivePath, encrypt,
trustTimes, maxSymlinkDepth)
trustTimes, maxSymlinkDepth, dryRun)
nUploadErrors += nErrs
message("Done.")

Expand Down

0 comments on commit 7560452

Please sign in to comment.