-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgdrive-stash.ps1
507 lines (415 loc) · 15.7 KB
/
gdrive-stash.ps1
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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
<#
MIT License
Copyright (c) 2024 pilgrim_tabby
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#>
<#
.SYNOPSIS
Copy a directory's files to a directory in Google Drive.
.DESCRIPTION
An add-on for the gdrive CLI (https://github.com/glotlabs/gdrive).
Loops through files in a local directory $srcDir (and its subdirectories if
$recursive is enabled) and puts them directly into a Google Drive directory
$destDir.
.PARAMETER srcDir
The locally stored parent directory of the directory to be backed up.
Ex: C:\foo\bar\ (trailing backslash optional)
.PARAMETER destDir
The full path OR the Google Drive ID of the target folder. Files inside srcDir
will be copied directly inside destDir. Using an ID is faster but the full path
is easier to obtain.
Paths begin with "\" (forward slashes are converted to backslashes in Resolve-
DriveDirId). Passing "\" will copy the files in srcDir into Google Drive's root
directory. If you want to copy files into a folder called "My Backups" in your
drive's root, you would pass destDir as "\My Backups".
.PARAMETER recursive
Tells function to recursively back up subdirectories in $srcDir.
Default is $false. Alias is "-r".
.PARAMETER makeParents
When enabled, tells Resolve-DriveDirId to create any directories in $destDir's
path that don't already exist. If this is disabled and $destDir doesn't exist,
the script will exit. Alias is "-p".
.PARAMETER destDirIsId
Tells function to treat $destDir as a Google Drive ID, not a pathname.
Default is $false. Alias is "-i".
.EXAMPLE
gdrive-stash "C:\foo\bar\mystuff" "\"
Result: All files in "mystuff" are copied into Google Drive's root directory,
excluding subdirectories.
.EXAMPLE
gdrive-stash "C:\foo\bar\mystuff" "\mybackups"
Result: All files in "mystuff", excluding subdirectories, are copied into
"mybackups", which resides in Google Drive's root directory. If "mybackups"
doesn't exist, the script will exit.
.EXAMPLE
gdrive-stash "C:\foo\bar\mystuff" "\mybackups\todays-date" -p
Result: All files in "mystuff", excluding subdirectories, are copied
into "mybackups\todays-date", which resides in Google Drive's root directory.
If either of those directories doesn't exist, they will be created.
.EXAMPLE
gdrive-stash "C:\foo\bar\mystuff" "\mybackups" -r
Result: All files in "mystuff", including subdirectories, are recursively
copied into "mybackups", which resides in Google Drive's root directory.
.EXAMPLE
gdrive-stash "C:\foo\bar\mystuff" "1Fn7xLIHE_iIY8o5MHbjQaAX20PdnE0ZD" -r -i
Result: All files in "mystuff", including subdirectories, are recursively
copied into the directory with Google Drive ID 1Fn7xLIHE_iIY8o5MHbjQaAX20PdnE0ZD.
This is much faster than having to crawl through an entire directory path, but
it may not be worth the effort...
#>
param (
[Parameter(Mandatory=$true)][string]$srcDir,
[Parameter(Mandatory=$true)][string]$destDir,
[switch][Alias("r")]$recursive,
[switch][Alias("p")]$makeParents,
[switch][Alias("i")]$destDirIsId
)
#########################
# #
# Function Declarations #
# #
#########################
function Backup-Dir {
<#
.SYNOPSIS
Back up a directory (and optionally, its subdirectories) to Google Drive.
.DESCRIPTION
Iterates through all non-directory files, calling Backup-File on each one.
If "recursive" is $true, also recursively backs up subdirectories.
See Get-Help gdrive-stash for parameter information and examples.
#>
param (
[Parameter(Mandatory=$true)][string]$srcDir,
[Parameter(Mandatory=$true)][string]$destDir,
[switch]$recursive,
[switch]$destDirIsId,
[switch]$makeParents
)
# Get list of files in $srcDir
$srcFiles = $(Get-LocalFileList $srcDir)
# Get destId and destFiles
if ($destDirIsId) {
$destId = $destDir
$destFiles = $(Get-DriveFileList $destId)
} else {
# Use splatting in case $makeParents isn't passed by calling function
$params = @{
destDir = $destDir
}
if ($makeParents) {
$params.makeParents = $makeParents
}
$destId, $destFiles = $(Resolve-DriveDirId @params)
}
# Iterate through files
foreach ($filename in $srcFiles) {
$fileType = $(Test-IsDir $srcDir $filename)
$fileId, $driveFileCreateTime = $(Get-DriveFileInfo $filename $fileType $destFiles)
# Either skip or recursively dive into directories, depending on -r
if ($fileType -eq ([FileType]::DIR)) {
if (!$recursive) { continue }
if ($fileId -ne "") {
$newDestId = $fileId
} else {
$params = "files", "mkdir", "--print-only-id"
if ($destId -ne "") {
$params += "--parent", $destId
}
$params += $filename
$newDestId = $(gdrive $params)
}
Backup-Dir "$srcDir\$filename" $newDestId -destDirIsId -recursive
# Back up all other file types directly
} else {
Backup-File $srcDir $filename $destId $fileId $driveFileCreateTime
}
}
}
function Get-LocalFileList {
<#
.SYNOPSIS
Return list of all files, including subdirectories, inside a directory.
.PARAMETER srcDir
The directory whose files are returned.
.OUTPUTS
[string] with the relative path to each file, separated by newlines.
#>
param (
[Parameter(Mandatory=$true)][string]$srcDir
)
try {
return $(Get-ChildItem $srcDir -Name)
} catch [System.Management.Automation.ItemNotFoundException] {
Write-Output "Error: source $srcDir is not a directory"
exit
}
}
function Get-DriveFileList {
<#
.SYNOPSIS
Return array of strings, each string containing info about a Drive file.
.DESCRIPTION
Gets $destDir's contents as a string, and splits the string into an array.
Each entry in the array contains the following information, in order:
-Google Drive ID
-Filename
-File type (folder, regular, document, etc.)
-File size (for directories, this is blank)
-Date and time file was created
The delimiter ":DELIMITER?" separates discreet pieces of information
in each entry in the array. This string is used because it's unlikely to be
used in a filename, and it uses a forbidden filename character for both
Windows ("?") and MacOS (":").
.PARAMETER destId
The directory holding the files that will be listed.
.OUTPUTS
[string[]]: Array of strings, each holding information about a file in
the directory at $destId
#>
param (
[Parameter(Mandatory=$true)][string]$destId
)
try {
$params =
"files",
"list",
"--field-separator", ":DELIMITER?",
"--order-by", "name",
"--skip-header",
"--full-name"
if ($destId -ne "") {
$params += "--parent", $destId
}
return $($(gdrive $params) -split "`n`r")
# Catch any invalid IDs
} catch [System.Management.Automation.RemoteException] {
Write-Output "Error: directory with ID $destId not found"
exit
}
}
function Get-DriveFileInfo {
<#
.SYNOPSIS
Search a Google Drive dir for a file matching a local file's name and type.
.DESCRIPTION
Make a case-sensitive search in a Google Drive directory for a file with a
given filename and type (Google Drive files aren't case sensitive, but
local files generally are). Possible file types are directory and file (see
enum FileType).
.PARAMETER filename
The name of the file to search for.
.PARAMETER fileType
The type of the file to search for.
Options are [FileType]::DIR and [FileType]::FILE.
.PARAMETER driveFileList
The array of file entries to search through. Each entry holds the following
information, in order:
-Google Drive ID
-Filename
-File type (folder, regular, document, etc.)
-File size (for directories, this is blank)
-Date and time file was created
If an empty string is passed for this parameter, the loop is skipped.
.OUTPUTS
[string]: Google Drive ID of the file. Blank if no match found.
[string]: The creation date and time of the Google Drive file. Blank if no
match found.
#>
param (
[Parameter(Mandatory=$true)][string]$filename,
[Parameter(Mandatory=$true)][FileType]$fileType,
[string[]]$driveFileList=@()
)
foreach ($line in $driveFileList) {
$fileInfo = $($line -split ":DELIMITER?", 0, "simplematch")
if ($fileInfo[1] -ceq $filename) {
if ($fileType -eq ([FileType]::DIR) -and $fileInfo[2] -eq "folder") {
return $fileInfo[0], $fileInfo[4]
} elseif ($fileType -eq ([FileType]::FILE) -and $fileInfo[2] -ne "folder") {
return $fileInfo[0], $fileInfo[4]
}
}
}
return "", ""
}
function Resolve-DriveDirId {
<#
.SYNOPSIS
Gets (or creates) the Google Drive ID for a directory.
.DESCRIPTION
Crawls the path $destDir to its last dir and returns its Google Drive ID.
If the path doesn't exist and makeParents is enabled, then all missing
directories are created.
.PARAMETER destDir
The directory whose ID will be extracted.
.PARAMETER makeParents
When $true, any directories in $destDir's path that don't exist will be
created (case-sensitive). If this option is off and $destDir doesn't exist,
the script exits.
.OUTPUTS
[string] currDirId: The Google Drive ID of $destDir.
[string[]] currFileList: List of info about files in dir at currDirId.
#>
param (
[Parameter(Mandatory=$true)][string]$destDir,
[switch]$makeParents
)
$params =
"files",
"list",
"--field-separator", ":DELIMITER?",
"--order-by", "name",
"--skip-header",
"--full-name"
$currFileList = $($(gdrive $params) -split "`n`r")
$currDirId = $prevDirId = "" # The return value if $destDir is root
# Standardize slashes, remove them from ends to simplify splitting
$dirsInPath = $destDir.Replace("/", "\").TrimStart("\").TrimEnd("\").Split("\")
# Special case -- user requested root dir as $destDir
if ([string]::IsNullOrEmpty($dirsInPath)) {
return $currDirId, $currFileList
}
foreach ($currDir in $dirsInPath) {
# We only use the first return value ($destId)
$currDirId = $(Get-DriveFileInfo $currDir ([FileType]::DIR) $currFileList)[0]
# Directory exists
if ($currDirId -ne "") {
$currFileList = $($(gdrive $params --parent $currDirId) -split "`n`r")
# Directory doesn't exist
} elseif ($makeParents) {
# Current directory is not root -- make new dir in most recent dir
if ($prevDirId -ne "") {
$currDirId = $(gdrive files mkdir --parent $prevDirId --print-only-id $currDir)
# Current directory is root -- make new directory in root
} else {
$currDirId = $(gdrive files mkdir --print-only-id $currDir)
}
# File list is empty, since the new directory is empty
$currFileList = @()
} else {
Write-Output "Error: destination $destDir is not a directory (case-sensitive)"
Write-Output "Use option `"-p`" to recursively create parent dirs"
exit
}
$prevDirId = $currDirId
}
return $currDirId, $currFileList
}
function Backup-File {
<#
.SYNOPSIS
Upload or update a local file into Google Drive.
.DESCRIPTION
If a file already exists in the directory at $destId, we check if its last
write time is more recent than the Drive file's creation time. If so, we
know it has been modified, so we delete and re-upload it.
Deleting and re-uploading is only marginally slower than simply updating,
and it allows us to reset the Drive file's creation date, since that value
is set to be the time of upload. This lets us compare that time with the
local write time later on to see if changes have been made (it's much more
difficult to access Drive files' most recent write date).
If a file doesn't exist in the dir at $destId yet, then upload it.
.PARAMETER srcDir
The parent directory of the file to back up.
.PARAMETER filename
The name of the file to back up, e.g. myfile.txt.
.PARAMETER destId
The Google Drive ID of the folder we are copying files into. If blank, this
means Google Drive's root directory.
.PARAMETER fileId
The Google Drive ID of the file we are dealing with, if the file already
exists in the dir at $destId. If not passed, the default is an empty string.
.PARAMETER driveFileCreateTime
The date and time at which the file in Google Drive was uploaded, if it
exists. If not passed, the default is an empty string.
#>
param (
[Parameter(Mandatory=$true)][string]$srcDir,
[Parameter(Mandatory=$true)][string]$filename,
[string]$destId="",
[string]$fileId="",
[string]$driveFileCreateTime=""
)
$upload_params = "files", "upload"
if ($destId -ne "") {
$upload_params += "--parent", $destId
}
$upload_params += "$srcDir\$filename"
# Pre-existing file
if ($fileId -ne "") {
$localWriteTime = (Get-ChildItem "$srcDir\$filename").
LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
if ($localWriteTime -gt $driveFileCreateTime) {
gdrive files delete $fileId
gdrive $upload_params
}
# New file
} else {
gdrive $upload_params
}
}
function Test-IsDir {
<#
.SYNOPSIS
Categorize a file into one of two types: non-dir file or dir.
See enum FileType.
.PARAMETER srcDir
The file's parent directory. Ex: C:\foo\ (trailing backslash not required)
.PARAMETER filename
The file's name, including extension. Ex: my_file.txt
.OUTPUTS
[FileType]::DIR if $srcDr\$filename is a dir, otherwise [FileType]::FILE.
#>
param (
[Parameter(Mandatory=$true)][string]$srcDir,
[Parameter(Mandatory=$true)]$filename
)
if ($(Get-Item "$srcDir\$filename").PSIsContainer) {
return ([FileType]::DIR)
}
return ([FileType]::FILE)
}
enum FileType {
<#
.SYNOPSIS
Categorize a file into one of two types: non-dir file or dir.
#>
DIR
FILE
}
##########
# #
# Script #
# #
##########
# Make sure non-terminating exceptions are caught
$ErrorActionPreference = "Stop"
# Parse parameters
$params = @{
srcDir = $srcDir
destDir = $destDir
}
if ($recursive) {
$params.recursive = $recursive
}
if ($makeParents) {
$params.makeParents = $makeParents
}
if ($destDirIsId) {
$params.destDirIsId = $destDirIsId
}
Backup-Dir @params