Post

Copy sharepoint data to Azure Filez

Copy sharepoint data to Azure Filez

Summary: This PowerShell script copies files and folders from a SharePoint site into an Azure Files share. It supports a -DryRun mode to preview the operations without making changes. The script enumerates folders and files in SharePoint, creates matching directories in Azure Files, downloads files into a temporary location, uploads them into the Azure Files share, and keeps simple statistics about folders and files processed.

How to run (quick)

  1. Save the full script below as Copy-SharePoint-to-AzureFiles.ps1.
  2. Edit the Configuration variables near the top of the script (storage account, file share, client ID, etc.).
  3. Test with a dry run:
1
powershell -ExecutionPolicy Bypass -File .\Copy-SharePoint-to-AzureFiles.ps1 -DryRun
  1. When the dry run looks good, run the real copy:
1
powershell -ExecutionPolicy Bypass -File .\Copy-SharePoint-to-AzureFiles.ps1
flowchart TD
  A([<i class="fa fa-play"></i> Start])
  B{<i class="fa fa-flask"></i> DryRun?}
  C[<i class="fa fa-list"></i> Scan SharePoint<br/>List operations]
  D[<i class="fa fa-link"></i> Connect to SharePoint]
  E[<i class="fa fa-folder-open"></i> Enumerate files & folders]
  F{<i class="fa fa-file-circle-question"></i> Item Type}
  G[<i class="fa fa-file-arrow-down"></i> Download file<br/><i class="fa fa-cloud-arrow-up"></i> Upload to Azure Files]
  H[<i class="fa fa-folder-plus"></i> Ensure Azure dir<br/>Recurse]
  I[<i class="fa fa-chart-line"></i> Update stats]
  J([<i class="fa fa-flag-checkered"></i> Finish & Report])

  A --> B
  B -- Yes --> C
  B -- No --> D
  D --> E
  E --> F
  F -- File --> G
  F -- Folder --> H
  H --> E
  G --> I
  C --> I
  I --> J

  %% Styles
  classDef startEnd fill:#2ecc71,color:#fff,stroke:#1e8449,stroke-width:2px
  classDef decision fill:#f1c40f,color:#000,stroke:#b7950b,stroke-width:2px
  classDef process fill:#3498db,color:#fff,stroke:#21618c
  classDef io fill:#9b59b6,color:#fff,stroke:#6c3483
  classDef stats fill:#1abc9c,color:#fff,stroke:#117a65

  class A,J startEnd
  class B,F decision
  class C,D,E,H process
  class G io
  class I stats

Full script — PowerShell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
<#
.SYNOPSIS
    Copy files from SharePoint site to Azure Files with automatic folder creation
.PARAMETER DryRun
    When specified, the script will only list what would be copied without actually copying anything
.EXAMPLE
    .\Copy-SharePoint-to-AzureFiles.ps1 -DryRun
    Run in dry run mode to preview all operations
.EXAMPLE
    .\Copy-SharePoint-to-AzureFiles.ps1
    Execute the actual copy operation
#>

param(
    [switch]$DryRun
)

# Global statistics
$script:Stats = @{
    FoldersToCreate = 0
    FilesToCopy = 0
    TotalSize = 0
    FoldersList = @()
    FilesList = @()
}

# Configuration - SharePoint Source
$SourceSiteUrl = "https://xxxx.sharepoint.com/sites/xxxx"
$ClientId = "xxxxxxxx-xxxxx-xxxx"
$UseInteractiveLogin = $true # Set to $true to use Interactive login (recommended for Global Admin)
$SourceLibrary = "" # Empty - folders are at site root level
$SourceRootFolder = "" # Empty because the site URL already points to folder_structure
$SubFolders = @("xxxxx", "xxxxx", "xxxx", "xxxxx", "xxxxx")

# Configuration - Azure Files Destination
$StorageAccountName = "xx"  # Change this
$FileShareName = "xx"            # Change this
$StorageAccountKey = "xxx"     # Change this or use connection string
$DestinationRootFolder = "xxxxx" # Root folder in Azure Files

# Temporary download location
$TempDownloadPath = "$env:TEMP\SharePointToAzureFiles"

# Function to ensure Azure Files directory exists
function Initialize-AzureFilesDirectory {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DirectoryPath,
        [Parameter(Mandatory = $true)]
        $Context,
        [switch]$DryRun
    )
    
    if ($DryRun) {
        Write-Host "[DRY RUN] Would check/create directory: $DirectoryPath" -ForegroundColor Cyan
        return
    }
    
    $pathParts = $DirectoryPath.Trim('/').Split('/')
    $currentPath = ""
    
    foreach ($part in $pathParts) {
        if ($currentPath -eq "") {
            $currentPath = $part
        } else {
            $currentPath = "$currentPath/$part"
        }
        
        # Check if directory exists
        $dir = Get-AzStorageFile -ShareName $FileShareName -Path $currentPath -Context $Context -ErrorAction SilentlyContinue
        
        if ($null -eq $dir) {
            Write-Host "Creating directory: $currentPath" -ForegroundColor Yellow
            try {
                $parentPath = if ($currentPath.Contains('/')) {
                    $currentPath.Substring(0, $currentPath.LastIndexOf('/'))
                } else {
                    $null
                }
                
                if ($parentPath) {
                    New-AzStorageDirectory -ShareName $FileShareName -Path $currentPath -Context $Context -ErrorAction Stop | Out-Null
                } else {
                    New-AzStorageDirectory -ShareName $FileShareName -Path $part -Context $Context -ErrorAction Stop | Out-Null
                }
                
                Write-Host "Created directory: $currentPath" -ForegroundColor Green
            }
            catch {
                Write-Host "Error creating directory $currentPath : $_" -ForegroundColor Red
            }
        }
    }
}

# Function to download and upload files recursively
function Copy-FilesRecursively {
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourcePath,
        [Parameter(Mandatory = $true)]
        [string]$DestinationPath,
        [Parameter(Mandatory = $false)]
        $AzureContext,
        [switch]$DryRun
    )
    
    if ($DryRun) {
        Write-Host "`n[DRY RUN] Scanning folder: $SourcePath" -ForegroundColor Magenta
    } else {
        Write-Host "`nProcessing folder: $SourcePath" -ForegroundColor Magenta
    }
    
    # Debug: Show the exact path being accessed
    $fullPath = if ($SourceLibrary -eq "") { $SourcePath } else { "$SourceLibrary/$SourcePath" }
    Write-Host "Debug: Attempting to access: $fullPath" -ForegroundColor Gray
    
    # Get all items in the current folder from SharePoint
    try {
        if ($SourceLibrary -eq "") {
            $items = Get-PnPFolderItem -FolderSiteRelativeUrl $SourcePath -ItemType All -ErrorAction Stop
        } else {
            $items = Get-PnPFolderItem -FolderSiteRelativeUrl "$SourceLibrary/$SourcePath" -ItemType All -ErrorAction Stop
        }
    }
    catch {
        Write-Host "Error accessing folder $SourcePath : $($_.Exception.Message)" -ForegroundColor Red
        return
    }
    
    if ($null -eq $items) {
        Write-Host "No items found in: $SourcePath" -ForegroundColor Yellow
        return
    }
    
    # Process files
    foreach ($file in $items | Where-Object { $_.GetType().Name -eq "File" }) {
        $fileSize = [math]::Round($file.Length / 1MB, 2)
        
        if ($DryRun) {
            Write-Host "[DRY RUN] Would copy file: $($file.Name) (Size: $fileSize MB)" -ForegroundColor White
            $script:Stats.FilesToCopy++
            $script:Stats.TotalSize += $file.Length
            $script:Stats.FilesList += [PSCustomObject]@{
                Name = $file.Name
                Source = "$SourcePath/$($file.Name)"
                Destination = "$DestinationPath/$($file.Name)"
                Size = $fileSize
            }
        } else {
            Write-Host "Copying file: $($file.Name) (Size: $fileSize MB)" -ForegroundColor White
            
            try {
                # Download file from SharePoint to temp location
                $tempFilePath = Join-Path $TempDownloadPath $file.Name
                $fileUrl = if ($SourceLibrary -eq "") { "$SourcePath/$($file.Name)" } else { "$SourceLibrary/$SourcePath/$($file.Name)" }
                Get-PnPFile -Url $fileUrl -AsFile -Path $TempDownloadPath -Filename $file.Name -Force -ErrorAction Stop | Out-Null
                
                # Upload to Azure Files
                Set-AzStorageFileContent -ShareName $FileShareName -Source $tempFilePath -Path "$DestinationPath/$($file.Name)" -Context $AzureContext -Force -ErrorAction Stop | Out-Null
                
                # Clean up temp file
                Remove-Item $tempFilePath -Force -ErrorAction SilentlyContinue
                
                Write-Host "Successfully copied: $($file.Name)" -ForegroundColor Green
            }
            catch {
                Write-Host "Error copying file $($file.Name): $_" -ForegroundColor Red
            }
        }
    }
    
    # Process subfolders recursively
    foreach ($folder in $items | Where-Object { $_.GetType().Name -eq "Folder" }) {
        $newSourcePath = "$SourcePath/$($folder.Name)"
        $newDestinationPath = "$DestinationPath/$($folder.Name)"
        
        if (-not $DryRun) {
            # Ensure Azure Files directory exists
            Initialize-AzureFilesDirectory -DirectoryPath $newDestinationPath -Context $AzureContext
        } else {
            $script:Stats.FoldersToCreate++
            if ($newDestinationPath -notin $script:Stats.FoldersList) {
                $script:Stats.FoldersList += $newDestinationPath
            }
        }
        
        Copy-FilesRecursively -SourcePath $newSourcePath -DestinationPath $newDestinationPath -AzureContext $AzureContext -DryRun:$DryRun
    }
}

# Main script execution
try {
    if ($DryRun) {
        Write-Host "`n========================================" -ForegroundColor Yellow
        Write-Host "DRY RUN MODE - No changes will be made" -ForegroundColor Yellow
        Write-Host "========================================`n" -ForegroundColor Yellow
    }
    
    Write-Host "Starting copy process..." -ForegroundColor Cyan
    Write-Host "Source: $SourceSiteUrl" -ForegroundColor Cyan
    Write-Host "Destination: Azure Files - $StorageAccountName/$FileShareName" -ForegroundColor Cyan
    
    # Connect to SharePoint
    Write-Host "`nConnecting to SharePoint source site..." -ForegroundColor Yellow
    Write-Host "A browser window will open for authentication..." -ForegroundColor Cyan
    try {
        if ($UseInteractiveLogin) {
            # Use Interactive login (browser-based authentication)
            Connect-PnPOnline -Url $SourceSiteUrl -ClientId $ClientId -ErrorAction Stop
        } else {
            Connect-PnPOnline -Url $SourceSiteUrl -ClientId $ClientId -ErrorAction Stop
        }
        Write-Host "Connected to SharePoint successfully" -ForegroundColor Green
    }
    catch {
        Write-Host "Failed to connect to SharePoint: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host "Make sure pop-ups are allowed in your browser" -ForegroundColor Yellow
        throw
    }
    
    # Connect to Azure Storage (only for actual copy, not dry run)
    if (-not $DryRun) {
        Write-Host "`nConnecting to Azure Storage..." -ForegroundColor Yellow
        $azureContext = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey
        Write-Host "Connected to Azure Storage successfully" -ForegroundColor Green
        
        # Ensure temp download directory exists
        if (-not (Test-Path $TempDownloadPath)) {
            New-Item -ItemType Directory -Path $TempDownloadPath -Force | Out-Null
        }
    } else {
        $azureContext = $null
    }
    
    # Process each subfolder
    foreach ($subFolder in $SubFolders) {
        Write-Host "`n========================================" -ForegroundColor Cyan
        Write-Host "Processing subfolder: $subFolder" -ForegroundColor Cyan
        Write-Host "========================================" -ForegroundColor Cyan
        
        # Build paths
        $sourcePath = if ($SourceRootFolder -eq "") { $subFolder } else { "$SourceRootFolder/$subFolder" }
        $destinationPath = if ($DestinationRootFolder -eq "") { $subFolder } else { "$DestinationRootFolder/$subFolder" }
        
        # Ensure root destination folder exists (only for non-dry run)
        if (-not $DryRun) {
            Initialize-AzureFilesDirectory -DirectoryPath $destinationPath -Context $azureContext
        }
        
        # Start recursive processing
        if ($DryRun) {
            Copy-FilesRecursively -SourcePath $sourcePath -DestinationPath $destinationPath -AzureContext $azureContext -DryRun
        } else {
            Copy-FilesRecursively -SourcePath $sourcePath -DestinationPath $destinationPath -AzureContext $azureContext
        }
    }
    
    if ($DryRun) {
        # Display summary statistics
        Write-Host "`n========================================" -ForegroundColor Yellow
        Write-Host "DRY RUN SUMMARY" -ForegroundColor Yellow
        Write-Host "========================================" -ForegroundColor Yellow
        Write-Host "Total folders to create: $($script:Stats.FoldersToCreate)" -ForegroundColor Cyan
        Write-Host "Total files to copy: $($script:Stats.FilesToCopy)" -ForegroundColor Cyan
        $totalSizeGB = [math]::Round($script:Stats.TotalSize / 1GB, 2)
        $totalSizeMB = [math]::Round($script:Stats.TotalSize / 1MB, 2)
        Write-Host "Total size to copy: $totalSizeMB MB ($totalSizeGB GB)" -ForegroundColor Cyan
        
        Write-Host "`n--- Folders to be created ---" -ForegroundColor Yellow
        if ($script:Stats.FoldersList.Count -gt 0) {
            $script:Stats.FoldersList | ForEach-Object { Write-Host "  + $_" -ForegroundColor DarkYellow }
        } else {
            Write-Host "  (No new folders needed)" -ForegroundColor Gray
        }
        
        Write-Host "`n--- Files to be copied ---" -ForegroundColor Yellow
        if ($script:Stats.FilesList.Count -gt 0) {
            $script:Stats.FilesList | ForEach-Object { 
                Write-Host "  $($_.Name)" -ForegroundColor White
                Write-Host "    From: $($_.Source)" -ForegroundColor Gray
                Write-Host "    To:   $($_.Destination)" -ForegroundColor Gray
                Write-Host "    Size: $($_.Size) MB" -ForegroundColor Gray
            }
        } else {
            Write-Host "  (No files to copy)" -ForegroundColor Gray
        }
        
        Write-Host "`n========================================" -ForegroundColor Green
        Write-Host "DRY RUN COMPLETED - Ready to execute" -ForegroundColor Green
        Write-Host "Run without -DryRun to perform actual copy" -ForegroundColor Green
        Write-Host "========================================" -ForegroundColor Green
    } else {
        # Clean up temp directory
        if (Test-Path $TempDownloadPath) {
            Remove-Item $TempDownloadPath -Recurse -Force -ErrorAction SilentlyContinue
        }
        
        Write-Host "`n========================================" -ForegroundColor Green
        Write-Host "Copy process completed!" -ForegroundColor Green
        Write-Host "========================================" -ForegroundColor Green
    }
}
catch {
    Write-Host "Error in main execution: $_" -ForegroundColor Red
    Write-Host $_.Exception.Message -ForegroundColor Red
}
finally {
    Disconnect-PnPOnline -ErrorAction SilentlyContinue
}
This post is licensed under CC BY 4.0 by the author.