storage/powershell/Copy-GcsObject.ps1 (338 lines of code) (raw):

# Copyright(c) 2016 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of # the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. ############################################################################## #.SYNOPSIS # Copies files to or from Google Cloud Storage. # #.DESCRIPTION # # Requires the Google Cloud SDK be installed. # # Google Cloud Storage paths look like: # gs://bucket/a/b/c.txt # # Does not support wildcards like * or ?. # Does not support paths with . or .. # #.PARAMETER SourcePath # The file or directory to copy. # #.PARAMETER DestPath # The location to copy to. # #.PARAMETER Force # Copy over existing files. # #.PARAMETER Recurse # Recursively copy the files in directories. # #.OUTPUTS # The newly created files. ############################################################################## param( [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()]$SourcePath, [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $DestPath, [switch] $Force, [switch] $Recurse) # Google Cloud Storage has no concept of folder. So to simulate having a # folder named foo, we create an empty file name foo_$folder$. Many tools # use this convention. $SCRIPT:FOLDER_SUFFIX = '_$folder$' function Make-FolderName([string] $Path) { if ($Path.EndsWith($SCRIPT:FOLDER_SUFFIX)) { return $Path } if ($Path.EndsWith('/') -or $Path.EndsWith('\')) { $Path = $Path.Substring(0, $Path.Length - 1); } return "$Path$SCRIPT:FOLDER_SUFFIX" } ############################################################################## #.SYNOPSIS # Splits a Cloud Storage path into its bucket name and object name. # #.PARAMETER Path # A Google Cloud Storage path, or not. Slashes can be forward or backward. # #.OUTPUTS # If Path is a valid Cloud Storage path, outputs two items: # bucket name # object name # Otherwise, outputs nothing. ############################################################################## function Split-GcsPath([string] $Path) { $Path = $Path.replace('\', '/') if ($Path -match '^[gG][sS]:/(/?)([^/]+)/(.*)') { $matches[2], $matches[3] } } ############################################################################## #.SYNOPSIS # Appends a slash to the path if it doesn't already end in a slash. # #.PARAMETER Path # The path to append. # #.PARAMETER Slash # The slash character to append. # #.OUTPUTS # A path that always ends with a Slash. ############################################################################## function Append-Slash([string] $Path, [string]$Slash = '\') { if ($Path.EndsWith($Slash)) { $Path } else { "$Path$Slash" } } ############################################################################## #.SYNOPSIS # Creates a directory on cloud storage. # #.DESCRIPTION # There is no concept of a directory in Google Cloud Storage. To simulate a # directory, we create a zero-length file that ends in _$folder$. # # First checks to see if a directory by the same name already exists, and # reports no error if it does. # #.PARAMETER Path # The directory name. # #.PARAMETER Bucket # The Cloud Storage bucket. # #.OUTPUTS # The Cloud Storage object created. ############################################################################## function Make-GcsDirectory([string] $Path, [string] $Bucket, [switch] $Force) { $folder = Make-FolderName $Path if (-not $Force) { $existing = Test-GcsObject -Bucket $Bucket -ObjectName $folder if ($existing -and $existing.Size -eq 0) { return # Directory already exists. } } New-GcsObject -Bucket $Bucket -ObjectName $folder ` -Force:$Force -Value "" } ############################################################################## #.SYNOPSIS # Uploads an item from the local file system to Google Cloud Storage. # #.PARAMETER SourcePath # The local file system path to upload. # #.PARAMETER DestPath # The prefix of the Cloud Storage object name to be created. # #.PARAMETER Bucket # The Cloud Storage bucket to upload to. # #.OUTPUTS # The list of Cloud Storage objects created. ############################################################################## function Upload-Item([string] $SourcePath, [string] $DestPath, [string] $Bucket) { # Is the source path a file or a directory? Does the # destination directory already exist? It takes a lot of logic # to match the behavior of cp and copy. $DestDir = Append-Slash $DestPath '/' $DestFolder = Make-FolderName $DestPath if (Test-Path -Path $SourcePath -PathType Leaf) { # It's a file. if (Test-GcsObject $Bucket $DestFolder) { # Copying a single file to a directory. New-GcsObject -Bucket $Bucket ` -ObjectName "$DestDir$(Split-Path $SourcePath -Leaf)" ` -File $SourcePath -Force:$Force } else { # Copying a single file to a file name. New-GcsObject -Bucket $Bucket -ObjectName $DestPath ` -File $SourcePath -Force:$Force } } elseif (Test-Path -Path $SourcePath -PathType Container) { # It's a directory. if (-not $Recurse) { throw [System.IO.FileNotFoundException] ` "Use the -Recurse flag to copy directories." } if (Test-GcsObject $Bucket $DestFolder) { # Copying a directory to an existing directory. $DestDir = "$DestDir$(Split-Path $SourcePath -Leaf)/" } Make-GcsDirectory -Bucket $Bucket -Path $DestDir -Force $Force Upload-Dir $SourcePath $DestDir $Bucket } else { throw [System.IO.FileNotFoundException] ` "$SourcePath does not exist." } } ############################################################################## #.SYNOPSIS # Uploads a directory local file system to Google Cloud Storage. # #.PARAMETER SourcePath # The local file system directory to upload. # #.PARAMETER DestDir # The prefix of the Cloud Storage object name to be created. Must end in /. # #.PARAMETER Bucket # The Cloud Storage bucket to upload to. # #.OUTPUTS # The list of Cloud Storage objects created. ############################################################################## function Upload-Dir([string] $SourcePath, [string] $DestDir, [string] $Bucket) { $sourceDir = Append-Slash $SourcePath '\' $items = Get-ChildItem $sourceDir | Sort-Object -Property Mode,Name foreach ($item in $items) { if (Test-Path -Path $item.FullName -PathType Container) { Make-GcsDirectory -Bucket $Bucket -Path "$DestDir$($item.Name)/" ` -Force $Force Upload-Dir "$sourceDir$($item.Name)" "$DestDir$($item.Name)/" ` $Bucket } else { New-GcsObject -Bucket $Bucket -ObjectName "$DestDir$($item.Name)" ` -File $item.FullName -Force:$Force } } } ############################################################################## #.SYNOPSIS # Downloads an object from Google Cloud Storage to the local file system. # #.PARAMETER SourcePath # The prefix of the Cloud Storage object name to be downloaded. # #.PARAMETER DestPath # The local file system path to create. # #.PARAMETER Bucket # The Cloud Storage bucket to download from. # #.OUTPUTS # The list of local files created. ############################################################################## function Download-Object([string] $SourcePath, [string] $DestPath, [string] $Bucket, [switch] $ShowProgress) { $outFile = if (Test-Path -Path $DestPath -PathType Container) { Join-Path $DestPath (Split-Path $SourcePath -Leaf) } else { $DestPath } if (-not $SourcePath.EndsWith('/') ` -and (Test-GcsObject $Bucket $SourcePath)) { # Source path is a simple file. Read-GcsObject -Bucket $Bucket -ObjectName $SourcePath ` -OutFile ([System.IO.Path]::GetFullPath($outFile)) -Force:$Force Get-Item $outFile } else { # Source is a directory. if (-not $Recurse) { throw [System.IO.FileNotFoundException] ` "Use the -Recurse flag to copy directories." } if (-not (Test-Path -Path $outFile -PathType Container)) { New-Item -Path $outFile -ItemType Directory } Download-Dir $SourcePath $outFile $Bucket -ShowProgress:$ShowProgress } } ############################################################################## #.SYNOPSIS # Downloads a directory from Google Cloud Storage to the local file system. # #.PARAMETER SourcePath # The prefix of the Cloud Storage object name to be downloaded. # #.PARAMETER DestPath # The local file system path to create. # #.PARAMETER Bucket # The Cloud Storage bucket to download from. # #.OUTPUTS # The list of local files created. ############################################################################## function Download-Dir([string] $SourcePath, [string] $DestPath, [string] $Bucket, [switch]$ShowProgress) { $sourceDir = Append-Slash $SourcePath '/' if ($ShowProgress) { $progress = 0 Write-Progress -Activity "Downloading objects" ` -CurrentOperation "Finding objects" -PercentComplete 0 } $objects = Get-GcsObject -Bucket $Bucket -Prefix $sourceDir foreach ($object in $objects) { if ($ShowProgress) { Write-Progress -Activity "Downloading objects" ` -CurrentOperation "Downloading $($object.Name)" ` -PercentComplete (++$progress * 100 / $objects.Length) ` -Completed:($progress -eq $objects.Length) } $relPath = $object.Name.Substring( $sourceDir.Length, $object.Name.Length - $sourceDir.Length) if ($relPath.Length -eq 0) { continue } $destFilePath = (Join-Path $DestPath $relPath) $DestDirPath = (Split-Path -Path $destFilePath) if ($relPath.EndsWith($SCRIPT:FOLDER_SUFFIX)) { # It's a directory New-Item -ItemType Directory -Force -Path $destFilePath.Substring( 0, $destFilePath.Length - $SCRIPT:FOLDER_SUFFIX.Length) } else { # It's a file $DestDir = New-Item -ItemType Directory -Force -Path $DestDirPath Read-GcsObject -Bucket $Bucket -ObjectName $object.Name ` -OutFile ([System.IO.Path]::GetFullPath($destFilePath)) ` -Force:$Force Get-Item $destFilePath } } } function Main { $destBucketAndPath = Split-GcsPath $DestPath $sourceBucketAndPath = Split-GcsPath $SourcePath if ($sourceBucketAndPath) { $sourceBucket, $sourcePath = $sourceBucketAndPath if ($destBucketAndPath) { # Copying from Cloud Storage to Cloud Storage. # Download to a temp directory, then upload. $sourceName = Split-Path $SourcePath -Leaf $tempParentPath = [System.IO.Path]::Combine( $env:TEMP, 'GcsCopies', (Get-Random)) $tempParentDir = New-Item -ItemType Directory -Path $tempParentPath $tempPath = [System.IO.Path]::Combine($tempParentPath, $sourceName) $localFiles = Download-Object $sourcePath $tempPath $sourceBucket ` -ShowProgress Upload-Item $tempPath $destBucketAndPath[1] $destBucketAndPath[0] } else { Download-Object $sourcePath $DestPath $sourceBucket } } else { if ($destBucketAndPath) { Upload-Item $SourcePath $destBucketAndPath[1] $destBucketAndPath[0] } else { # Both paths are local. Let the local file system do it. Copy-Item -Path $SourcePath -Destination $DestPath -Force:$Force ` -Recurse:$Recurse } } } if (Get-Command Get-GcsObject) { Main } else { Write-Warning "Requires the Google Cloud SDK. Download it from: https://cloud.google.com/sdk" }