-
-
Notifications
You must be signed in to change notification settings - Fork 859
/
sync.d
7741 lines (6882 loc) · 363 KB
/
sync.d
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
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// What is this module called?
module syncEngine;
// What does this module require to function?
import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;
import core.thread;
import core.time;
import std.algorithm;
import std.array;
import std.concurrency;
import std.container.rbtree;
import std.conv;
import std.datetime;
import std.encoding;
import std.exception;
import std.file;
import std.json;
import std.parallelism;
import std.path;
import std.range;
import std.regex;
import std.stdio;
import std.string;
import std.uni;
import std.uri;
import std.utf;
import std.math;
// What other modules that we have created do we need to import?
import config;
import log;
import util;
import onedrive;
import itemdb;
import clientSideFiltering;
class jsonResponseException: Exception {
@safe pure this(string inputMessage) {
string msg = format(inputMessage);
super(msg);
}
}
class posixException: Exception {
@safe pure this(string localTargetName, string remoteTargetName) {
string msg = format("POSIX 'case-insensitive match' between '%s' (local) and '%s' (online) which violates the Microsoft OneDrive API namespace convention", localTargetName, remoteTargetName);
super(msg);
}
}
class accountDetailsException: Exception {
@safe pure this() {
string msg = format("Unable to query OneDrive API to obtain required account details");
super(msg);
}
}
class SyncException: Exception {
@nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__) {
super(msg, file, line);
}
}
class SyncEngine {
// Class Variables
ApplicationConfig appConfig;
OneDriveApi oneDriveApiInstance;
ItemDatabase itemDB;
ClientSideFiltering selectiveSync;
// Array of directory databaseItem.id to skip while applying the changes.
// These are the 'parent path' id's that are being excluded, so if the parent id is in here, the child needs to be skipped as well
RedBlackTree!string skippedItems = redBlackTree!string();
// Array of databaseItem.id to delete after the changes have been downloaded
string[2][] idsToDelete;
// Array of JSON items which are files or directories that are not 'root', skipped or to be deleted, that need to be processed
JSONValue[] jsonItemsToProcess;
// Array of JSON items which are files that are not 'root', skipped or to be deleted, that need to be downloaded
JSONValue[] fileJSONItemsToDownload;
// Array of paths that failed to download
string[] fileDownloadFailures;
// Array of all OneDrive driveId's that have been seen
string[] driveIDsArray;
// List of items we fake created when using --dry-run
string[2][] idsFaked;
// List of paths we fake deleted when using --dry-run
string[] pathFakeDeletedArray;
// Array of database Parent Item ID, Item ID & Local Path where the content has changed and needs to be uploaded
string[3][] databaseItemsWhereContentHasChanged;
// Array of local file paths that need to be uploaded as new itemts to OneDrive
string[] newLocalFilesToUploadToOneDrive;
// Array of local file paths that failed to be uploaded to OneDrive
string[] fileUploadFailures;
// List of path names changed online, but not changed locally when using --dry-run
string[] pathsRenamed;
// List of paths that were a POSIX case-insensitive match, thus could not be created online
string[] posixViolationPaths;
// List of local paths, that, when using the OneDrive Business Shared Folders feature, then diabling it, folder still exists locally and online
// This list of local paths need to be skipped
string[] businessSharedFoldersOnlineToSkip;
// List of interrupted uploads session files that need to be resumed
string[] interruptedUploadsSessionFiles;
// List of validated interrupted uploads session JSON items to resume
JSONValue[] jsonItemsToResumeUpload;
// Flag that there were upload or download failures listed
bool syncFailures = false;
// Is sync_list configured
bool syncListConfigured = false;
// Was --dry-run used?
bool dryRun = false;
// Was --upload-only used?
bool uploadOnly = false;
// Was --remove-source-files used?
// Flag to set whether the local file should be deleted once it is successfully uploaded to OneDrive
bool localDeleteAfterUpload = false;
// Do we configure to disable the download validation routine due to --disable-download-validation
// We will always validate our downloads
// However, when downloading files from SharePoint, the OneDrive API will not advise the correct file size
// which means that the application thinks the file download has failed as the size is different / hash is different
// See: https://github.com/abraunegg/onedrive/discussions/1667
bool disableDownloadValidation = false;
// Do we configure to disable the upload validation routine due to --disable-upload-validation
// We will always validate our uploads
// However, when uploading a file that can contain metadata SharePoint will associate some
// metadata from the library the file is uploaded to directly in the file which breaks this validation.
// See: https://github.com/abraunegg/onedrive/issues/205
// See: https://github.com/OneDrive/onedrive-api-docs/issues/935
bool disableUploadValidation = false;
// Do we perform a local cleanup of files that are 'extra' on the local file system, when using --download-only
bool cleanupLocalFiles = false;
// Are we performing a --single-directory sync ?
bool singleDirectoryScope = false;
string singleDirectoryScopeDriveId;
string singleDirectoryScopeItemId;
// Is National Cloud Deployments configured ?
bool nationalCloudDeployment = false;
// Do we configure not to perform a remote file delete if --upload-only & --no-remote-delete configured
bool noRemoteDelete = false;
// Is bypass_data_preservation set via config file
// Local data loss MAY occur in this scenario
bool bypassDataPreservation = false;
// Maximum file size upload
// https://support.microsoft.com/en-us/office/invalid-file-names-and-file-types-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa?ui=en-us&rs=en-us&ad=us
// July 2020, maximum file size for all accounts is 100GB
// January 2021, maximum file size for all accounts is 250GB
ulong maxUploadFileSize = 268435456000; // 250GB
// Threshold after which files will be uploaded using an upload session
ulong sessionThresholdFileSize = 4 * 2^^20; // 4 MiB
// File size limit for file operations that the user has configured
ulong fileSizeLimit;
// Total data to upload
ulong totalDataToUpload;
// How many items have been processed for the active operation
ulong processedCount;
// Are we creating a simulated /delta response? This is critically important in terms of how we 'update' the database
bool generateSimulatedDeltaResponse = false;
// Store the latest DeltaLink
string latestDeltaLink;
// Configure this class instance
this(ApplicationConfig appConfig, ItemDatabase itemDB, ClientSideFiltering selectiveSync) {
// Configure the class varaible to consume the application configuration
this.appConfig = appConfig;
// Configure the class varaible to consume the database configuration
this.itemDB = itemDB;
// Configure the class variable to consume the selective sync (skip_dir, skip_file and sync_list) configuration
this.selectiveSync = selectiveSync;
// Configure the dryRun flag to capture if --dry-run was used
// Application startup already flagged we are also in a --dry-run state, so no need to output anything else here
this.dryRun = appConfig.getValueBool("dry_run");
// Configure file size limit
if (appConfig.getValueLong("skip_size") != 0) {
fileSizeLimit = appConfig.getValueLong("skip_size") * 2^^20;
fileSizeLimit = (fileSizeLimit == 0) ? ulong.max : fileSizeLimit;
}
// Is there a sync_list file present?
if (exists(appConfig.syncListFilePath)) this.syncListConfigured = true;
// Configure the uploadOnly flag to capture if --upload-only was used
if (appConfig.getValueBool("upload_only")) {
addLogEntry("Configuring uploadOnly flag to TRUE as --upload-only passed in or configured", ["debug"]);
this.uploadOnly = true;
}
// Configure the localDeleteAfterUpload flag
if (appConfig.getValueBool("remove_source_files")) {
addLogEntry("Configuring localDeleteAfterUpload flag to TRUE as --remove-source-files passed in or configured", ["debug"]);
this.localDeleteAfterUpload = true;
}
// Configure the disableDownloadValidation flag
if (appConfig.getValueBool("disable_download_validation")) {
addLogEntry("Configuring disableDownloadValidation flag to TRUE as --disable-download-validation passed in or configured", ["debug"]);
this.disableDownloadValidation = true;
}
// Configure the disableUploadValidation flag
if (appConfig.getValueBool("disable_upload_validation")) {
addLogEntry("Configuring disableUploadValidation flag to TRUE as --disable-upload-validation passed in or configured", ["debug"]);
this.disableUploadValidation = true;
}
// Do we configure to clean up local files if using --download-only ?
if ((appConfig.getValueBool("download_only")) && (appConfig.getValueBool("cleanup_local_files"))) {
// --download-only and --cleanup-local-files were passed in
addLogEntry();
addLogEntry("WARNING: Application has been configured to cleanup local files that are not present online.");
addLogEntry("WARNING: Local data loss MAY occur in this scenario if you are expecting data to remain archived locally.");
addLogEntry();
// Set the flag
this.cleanupLocalFiles = true;
}
// Do we configure to NOT perform a remote delete if --upload-only & --no-remote-delete configured ?
if ((appConfig.getValueBool("upload_only")) && (appConfig.getValueBool("no_remote_delete"))) {
// --upload-only and --no-remote-delete were passed in
addLogEntry("WARNING: Application has been configured NOT to cleanup remote files that are deleted locally.");
// Set the flag
this.noRemoteDelete = true;
}
// Are we forcing to use /children scan instead of /delta to simulate National Cloud Deployment use of /children?
if (appConfig.getValueBool("force_children_scan")) {
addLogEntry("Forcing client to use /children API call rather than /delta API to retrieve objects from the OneDrive API");
this.nationalCloudDeployment = true;
}
// Are we forcing the client to bypass any data preservation techniques to NOT rename any local files if there is a conflict?
// The enabling of this function could lead to data loss
if (appConfig.getValueBool("bypass_data_preservation")) {
addLogEntry("WARNING: Application has been configured to bypass local data preservation in the event of file conflict.");
addLogEntry("WARNING: Local data loss MAY occur in this scenario.");
this.bypassDataPreservation = true;
}
// Did the user configure a specific rate limit for the application?
if (appConfig.getValueLong("rate_limit") > 0) {
// User configured rate limit
addLogEntry("User Configured Rate Limit: " ~ to!string(appConfig.getValueLong("rate_limit")));
// If user provided rate limit is < 131072, flag that this is too low, setting to the recommended minimum of 131072
if (appConfig.getValueLong("rate_limit") < 131072) {
// user provided limit too low
addLogEntry("WARNING: User configured rate limit too low for normal application processing and preventing application timeouts. Overriding to recommended minimum of 131072 (128KB/s)");
appConfig.setValueLong("rate_limit", 131072);
}
}
// Did the user downgrade all HTTP operations to force HTTP 1.1
if (appConfig.getValueBool("force_http_11")) {
// User is forcing downgrade to curl to use HTTP 1.1 for all operations
addLogEntry("Downgrading all HTTP operations to HTTP/1.1 due to user configuration", ["verbose"]);
} else {
// Use curl defaults
addLogEntry("Using Curl defaults for HTTP operational protocol version (potentially HTTP/2)", ["debug"]);
}
}
// Initialise the Sync Engine class
bool initialise() {
// create a new instance of the OneDrive API
oneDriveApiInstance = new OneDriveApi(appConfig);
if (oneDriveApiInstance.initialise()) {
try {
// Get the relevant default account & drive details
getDefaultDriveDetails();
} catch (accountDetailsException exception) {
// details could not be queried
addLogEntry(exception.msg);
// Shutdown API instance
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
// Must force exit here, allow logging to be done
forceExit();
}
try {
// Get the relevant default account & drive details
getDefaultRootDetails();
} catch (accountDetailsException exception) {
// details could not be queried
addLogEntry(exception.msg);
// Shutdown API instance
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
// Must force exit here, allow logging to be done
forceExit();
}
try {
// Display details
displaySyncEngineDetails();
} catch (accountDetailsException exception) {
// details could not be queried
addLogEntry(exception.msg);
// Shutdown API instance
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
// Must force exit here, allow logging to be done
forceExit();
}
} else {
// API could not be initialised
addLogEntry("OneDrive API could not be initialised with previously used details");
// Shutdown API instance
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
// Must force exit here, allow logging to be done
forceExit();
}
// API was initialised
addLogEntry("Sync Engine Initialised with new Onedrive API instance", ["verbose"]);
// Shutdown this API instance, as we will create API instances as required, when required
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
return true;
}
// Get Default Drive Details for this Account
void getDefaultDriveDetails() {
// Function variables
JSONValue defaultOneDriveDriveDetails;
// Get Default Drive Details for this Account
try {
addLogEntry("Getting Account Default Drive Details", ["debug"]);
defaultOneDriveDriveDetails = oneDriveApiInstance.getDefaultDriveDetails();
} catch (OneDriveException exception) {
addLogEntry("defaultOneDriveDriveDetails = oneDriveApiInstance.getDefaultDriveDetails() generated a OneDriveException", ["debug"]);
string thisFunctionName = getFunctionName!({});
if ((exception.httpStatusCode == 400) || (exception.httpStatusCode == 401)) {
// Handle the 400 | 401 error
handleClientUnauthorised(exception.httpStatusCode, exception.msg);
}
// HTTP request returned status code 408,429,503,504
if ((exception.httpStatusCode == 408) || (exception.httpStatusCode == 429) || (exception.httpStatusCode == 503) || (exception.httpStatusCode == 504)) {
// Handle the 429
if (exception.httpStatusCode == 429) {
// HTTP request returned status code 429 (Too Many Requests). We need to leverage the response Retry-After HTTP header to ensure minimum delay until the throttle is removed.
handleOneDriveThrottleRequest(oneDriveApiInstance);
addLogEntry("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry " ~ thisFunctionName, ["debug"]);
}
// re-try the specific changes queries
if ((exception.httpStatusCode == 408) ||(exception.httpStatusCode == 503) || (exception.httpStatusCode == 504)) {
// 408 - Request Time Out
// 503 - Service Unavailable
// 504 - Gateway Timeout
// Transient error - try again in 30 seconds
auto errorArray = splitLines(exception.msg);
addLogEntry(to!string(errorArray[0]) ~ " when attempting to query Account Default Drive Details - retrying applicable request in 30 seconds");
addLogEntry("defaultOneDriveDriveDetails = oneDriveApiInstance.getDefaultDriveDetails() previously threw an error - retrying", ["debug"]);
// The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request.
addLogEntry("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request", ["debug"]);
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429 and 504 - but loop back calling this function
addLogEntry("Retrying Function: getDefaultDriveDetails()", ["debug"]);
getDefaultDriveDetails();
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
}
}
// If the JSON response is a correct JSON object, and has an 'id' we can set these details
if ((defaultOneDriveDriveDetails.type() == JSONType.object) && (hasId(defaultOneDriveDriveDetails))) {
addLogEntry("OneDrive Account Default Drive Details: " ~ to!string(defaultOneDriveDriveDetails), ["debug"]);
appConfig.accountType = defaultOneDriveDriveDetails["driveType"].str;
appConfig.defaultDriveId = defaultOneDriveDriveDetails["id"].str;
// Get the initial remaining size from OneDrive API response JSON
// This will be updated as we upload data to OneDrive
if (hasQuota(defaultOneDriveDriveDetails)) {
if ("remaining" in defaultOneDriveDriveDetails["quota"]){
// use the value provided
appConfig.remainingFreeSpace = defaultOneDriveDriveDetails["quota"]["remaining"].integer;
}
}
// In some cases OneDrive Business configurations 'restrict' quota details thus is empty / blank / negative value / zero
if (appConfig.remainingFreeSpace <= 0) {
// free space is <= 0 .. why ?
if ("remaining" in defaultOneDriveDriveDetails["quota"]) {
if (appConfig.accountType == "personal") {
// zero space available
addLogEntry("ERROR: OneDrive account currently has zero space available. Please free up some space online.");
appConfig.quotaAvailable = false;
} else {
// zero space available is being reported, maybe being restricted?
addLogEntry("WARNING: OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator.");
appConfig.quotaRestricted = true;
}
} else {
// json response was missing a 'remaining' value
if (appConfig.accountType == "personal") {
addLogEntry("ERROR: OneDrive quota information is missing. Potentially your OneDrive account currently has zero space available. Please free up some space online.");
appConfig.quotaAvailable = false;
} else {
// quota details not available
addLogEntry("ERROR: OneDrive quota information is being restricted. Please fix by speaking to your OneDrive / Office 365 Administrator.");
appConfig.quotaRestricted = true;
}
}
}
// What did we set based on the data from the JSON
addLogEntry("appConfig.accountType = " ~ appConfig.accountType, ["debug"]);
addLogEntry("appConfig.defaultDriveId = " ~ appConfig.defaultDriveId, ["debug"]);
addLogEntry("appConfig.remainingFreeSpace = " ~ to!string(appConfig.remainingFreeSpace), ["debug"]);
addLogEntry("appConfig.quotaAvailable = " ~ to!string(appConfig.quotaAvailable), ["debug"]);
addLogEntry("appConfig.quotaRestricted = " ~ to!string(appConfig.quotaRestricted), ["debug"]);
// Make sure that appConfig.defaultDriveId is in our driveIDs array to use when checking if item is in database
// Keep the driveIDsArray with unique entries only
if (!canFind(driveIDsArray, appConfig.defaultDriveId)) {
// Add this drive id to the array to search with
driveIDsArray ~= appConfig.defaultDriveId;
}
} else {
// Handle the invalid JSON response
throw new accountDetailsException();
}
}
// Get Default Root Details for this Account
void getDefaultRootDetails() {
// Function variables
JSONValue defaultOneDriveRootDetails;
// Get Default Root Details for this Account
try {
addLogEntry("Getting Account Default Root Details", ["debug"]);
defaultOneDriveRootDetails = oneDriveApiInstance.getDefaultRootDetails();
} catch (OneDriveException exception) {
addLogEntry("defaultOneDriveRootDetails = oneDriveApiInstance.getDefaultRootDetails() generated a OneDriveException", ["debug"]);
string thisFunctionName = getFunctionName!({});
if ((exception.httpStatusCode == 400) || (exception.httpStatusCode == 401)) {
// Handle the 400 | 401 error
handleClientUnauthorised(exception.httpStatusCode, exception.msg);
}
// HTTP request returned status code 408,429,503,504
if ((exception.httpStatusCode == 408) || (exception.httpStatusCode == 429) || (exception.httpStatusCode == 503) || (exception.httpStatusCode == 504)) {
// Handle the 429
if (exception.httpStatusCode == 429) {
// HTTP request returned status code 429 (Too Many Requests). We need to leverage the response Retry-After HTTP header to ensure minimum delay until the throttle is removed.
handleOneDriveThrottleRequest(oneDriveApiInstance);
addLogEntry("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry " ~ thisFunctionName, ["debug"]);
}
// re-try the specific changes queries
if ((exception.httpStatusCode == 408) || (exception.httpStatusCode == 503) || (exception.httpStatusCode == 504)) {
// 503 - Service Unavailable
// 504 - Gateway Timeout
// Transient error - try again in 30 seconds
auto errorArray = splitLines(exception.msg);
addLogEntry(to!string(errorArray[0]) ~ " when attempting to query Account Default Root Details - retrying applicable request in 30 seconds");
addLogEntry("defaultOneDriveRootDetails = oneDriveApiInstance.getDefaultRootDetails() previously threw an error - retrying", ["debug"]);
// The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request.
addLogEntry("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request", ["debug"]);
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
addLogEntry("Retrying Function: " ~ thisFunctionName, ["debug"]);
getDefaultRootDetails();
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
}
}
// If the JSON response is a correct JSON object, and has an 'id' we can set these details
if ((defaultOneDriveRootDetails.type() == JSONType.object) && (hasId(defaultOneDriveRootDetails))) {
addLogEntry("OneDrive Account Default Root Details: " ~ to!string(defaultOneDriveRootDetails), ["debug"]);
appConfig.defaultRootId = defaultOneDriveRootDetails["id"].str;
addLogEntry("appConfig.defaultRootId = " ~ appConfig.defaultRootId, ["debug"]);
// Save the item to the database, so the account root drive is is always going to be present in the DB
saveItem(defaultOneDriveRootDetails);
} else {
// Handle the invalid JSON response
throw new accountDetailsException();
}
}
// Reset syncFailures to false
void resetSyncFailures() {
// Reset syncFailures to false if these are both empty
if (syncFailures) {
if ((fileDownloadFailures.empty) && (fileUploadFailures.empty)) {
addLogEntry("Resetting syncFailures = false");
syncFailures = false;
} else {
addLogEntry("File activity array's not empty - not resetting syncFailures");
}
}
}
// Perform a sync of the OneDrive Account
// - Query /delta
// - If singleDirectoryScope or nationalCloudDeployment is used we need to generate a /delta like response
// - Process changes (add, changes, moves, deletes)
// - Process any items to add (download data to local)
// - Detail any files that we failed to download
// - Process any deletes (remove local data)
void syncOneDriveAccountToLocalDisk() {
// performFullScanTrueUp value
addLogEntry("Perform a Full Scan True-Up: " ~ to!string(appConfig.fullScanTrueUpRequired), ["debug"]);
// Fetch the API response of /delta to track changes on OneDrive
fetchOneDriveDeltaAPIResponse(null, null, null);
// Process any download activities or cleanup actions
processDownloadActivities();
// If singleDirectoryScope is false, we are not targeting a single directory
// but if true, the target 'could' be a shared folder - so dont try and scan it again
if (!singleDirectoryScope) {
// OneDrive Shared Folder Handling
if (appConfig.accountType == "personal") {
// Personal Account Type
// https://github.com/OneDrive/onedrive-api-docs/issues/764
// Get the Remote Items from the Database
Item[] remoteItems = itemDB.selectRemoteItems();
foreach (remoteItem; remoteItems) {
// Check if this path is specifically excluded by 'skip_dir', but only if 'skip_dir' is not empty
if (appConfig.getValueString("skip_dir") != "") {
// The path that needs to be checked needs to include the '/'
// This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched
if (selectiveSync.isDirNameExcluded(remoteItem.name)) {
// This directory name is excluded
addLogEntry("Skipping item - excluded by skip_dir config: " ~ remoteItem.name, ["verbose"]);
continue;
}
}
// Directory name is not excluded or skip_dir is not populated
if (!appConfig.surpressLoggingOutput) {
addLogEntry("Syncing this OneDrive Personal Shared Folder: " ~ remoteItem.name);
}
// Check this OneDrive Personal Shared Folder for changes
fetchOneDriveDeltaAPIResponse(remoteItem.remoteDriveId, remoteItem.remoteId, remoteItem.name);
// Process any download activities or cleanup actions for this OneDrive Personal Shared Folder
processDownloadActivities();
}
} else {
// Is this a Business Account with Sync Business Shared Items enabled?
if ((appConfig.accountType == "business") && ( appConfig.getValueBool("sync_business_shared_items"))) {
// Business Account Shared Items Handling
// - OneDrive Business Shared Folder
// - OneDrive Business Shared Files ??
// - SharePoint Links
// Get the Remote Items from the Database
Item[] remoteItems = itemDB.selectRemoteItems();
foreach (remoteItem; remoteItems) {
// Check if this path is specifically excluded by 'skip_dir', but only if 'skip_dir' is not empty
if (appConfig.getValueString("skip_dir") != "") {
// The path that needs to be checked needs to include the '/'
// This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched
if (selectiveSync.isDirNameExcluded(remoteItem.name)) {
// This directory name is excluded
addLogEntry("Skipping item - excluded by skip_dir config: " ~ remoteItem.name, ["verbose"]);
continue;
}
}
// Directory name is not excluded or skip_dir is not populated
if (!appConfig.surpressLoggingOutput) {
addLogEntry("Syncing this OneDrive Business Shared Folder: " ~ remoteItem.name);
}
// Debug log output
addLogEntry("Fetching /delta API response for:", ["debug"]);
addLogEntry(" remoteItem.remoteDriveId: " ~ remoteItem.remoteDriveId, ["debug"]);
addLogEntry(" remoteItem.remoteId: " ~ remoteItem.remoteId, ["debug"]);
// Check this OneDrive Personal Shared Folder for changes
fetchOneDriveDeltaAPIResponse(remoteItem.remoteDriveId, remoteItem.remoteId, remoteItem.name);
// Process any download activities or cleanup actions for this OneDrive Personal Shared Folder
processDownloadActivities();
}
}
}
}
}
// Configure singleDirectoryScope = true if this function is called
// By default, singleDirectoryScope = false
void setSingleDirectoryScope(string normalisedSingleDirectoryPath) {
// Function variables
Item searchItem;
JSONValue onlinePathData;
// Set the main flag
singleDirectoryScope = true;
// What are we doing?
addLogEntry("The OneDrive Client was asked to search for this directory online and create it if it's not located: " ~ normalisedSingleDirectoryPath);
// Query the OneDrive API for the specified path online
// In a --single-directory scenario, we need to travervse the entire path that we are wanting to sync
// and then check the path element does it exist online, if it does, is it a POSIX match, or if it does not, create the path
// Once we have searched online, we have the right drive id and item id so that we can downgrade the sync status, then build up
// any object items from that location
// This is because, in a --single-directory scenario, any folder in the entire path tree could be a 'case-insensitive match'
try {
onlinePathData = queryOneDriveForSpecificPathAndCreateIfMissing(normalisedSingleDirectoryPath, true);
} catch (posixException e) {
displayPosixErrorMessage(e.msg);
addLogEntry("ERROR: Requested directory to search for and potentially create has a 'case-insensitive match' to an existing directory on OneDrive online.");
}
// Was a valid JSON response provided?
if (onlinePathData.type() == JSONType.object) {
// Valid JSON item was returned
searchItem = makeItem(onlinePathData);
addLogEntry("searchItem: " ~ to!string(searchItem), ["debug"]);
// Is this item a potential Shared Folder?
// Is this JSON a remote object
if (isItemRemote(onlinePathData)) {
// The path we are seeking is remote to our account drive id
searchItem.driveId = onlinePathData["remoteItem"]["parentReference"]["driveId"].str;
searchItem.id = onlinePathData["remoteItem"]["id"].str;
}
// Set these items so that these can be used as required
singleDirectoryScopeDriveId = searchItem.driveId;
singleDirectoryScopeItemId = searchItem.id;
} else {
addLogEntry();
addLogEntry("The requested --single-directory path to sync has generated an error. Please correct this error and try again.");
addLogEntry();
forceExit();
}
}
// Query OneDrive API for /delta changes and iterate through items online
void fetchOneDriveDeltaAPIResponse(string driveIdToQuery = null, string itemIdToQuery = null, string sharedFolderName = null) {
string deltaLink = null;
string currentDeltaLink = null;
string deltaLinkAvailable;
JSONValue deltaChanges;
ulong responseBundleCount;
ulong jsonItemsReceived = 0;
// Reset jsonItemsToProcess & processedCount
jsonItemsToProcess = [];
processedCount = 0;
// Was a driveId provided as an input
//if (driveIdToQuery == "") {
if (strip(driveIdToQuery).empty) {
// No provided driveId to query, use the account default
addLogEntry("driveIdToQuery was empty, setting to appConfig.defaultDriveId", ["debug"]);
driveIdToQuery = appConfig.defaultDriveId;
addLogEntry("driveIdToQuery: " ~ driveIdToQuery, ["debug"]);
}
// Was an itemId provided as an input
//if (itemIdToQuery == "") {
if (strip(itemIdToQuery).empty) {
// No provided itemId to query, use the account default
addLogEntry("itemIdToQuery was empty, setting to appConfig.defaultRootId", ["debug"]);
itemIdToQuery = appConfig.defaultRootId;
addLogEntry("itemIdToQuery: " ~ itemIdToQuery, ["debug"]);
}
// What OneDrive API query do we use?
// - Are we running against a National Cloud Deployments that does not support /delta ?
// National Cloud Deployments do not support /delta as a query
// https://docs.microsoft.com/en-us/graph/deployments#supported-features
//
// - Are we performing a --single-directory sync, which will exclude many items online, focusing in on a specific online directory
//
// - Are we performing a --download-only --cleanup-local-files action?
// - If we are, and we use a normal /delta query, we get all the local 'deleted' objects as well.
// - If the user deletes a folder online, then replaces it online, we download the deletion events and process the new 'upload' via the web iterface ..
// the net effect of this, is that the valid local files we want to keep, are actually deleted ...... not desirable
if ((singleDirectoryScope) || (nationalCloudDeployment) || (cleanupLocalFiles)) {
// Generate a simulated /delta response so that we correctly capture the current online state, less any 'online' delete and replace activity
generateSimulatedDeltaResponse = true;
}
// What /delta query do we use?
if (!generateSimulatedDeltaResponse) {
// This should be the majority default pathway application use
// Get the current delta link from the database for this DriveID and RootID
deltaLinkAvailable = itemDB.getDeltaLink(driveIdToQuery, itemIdToQuery);
if (!deltaLinkAvailable.empty) {
addLogEntry("Using database stored deltaLink", ["debug"]);
currentDeltaLink = deltaLinkAvailable;
}
// Do we need to perform a Full Scan True Up? Is 'appConfig.fullScanTrueUpRequired' set to 'true'?
if (appConfig.fullScanTrueUpRequired) {
addLogEntry("Performing a full scan of online data to ensure consistent local state");
addLogEntry("Setting currentDeltaLink = null", ["debug"]);
currentDeltaLink = null;
}
// Dynamic output for non-verbose and verbose run so that the user knows something is being retreived from the OneDrive API
if (appConfig.verbosityCount == 0) {
if (!appConfig.surpressLoggingOutput) {
addLogEntry("Fetching items from the OneDrive API for Drive ID: " ~ driveIdToQuery, ["logFileOnly"]);
// Use the dots to show the application is 'doing something'
addLogEntry("Fetching items from the OneDrive API for Drive ID: " ~ driveIdToQuery ~ " .", ["consoleOnlyNoNewLine"]);
}
} else {
addLogEntry("Fetching /delta response from the OneDrive API for Drive ID: " ~ driveIdToQuery, ["verbose"]);
}
// Create a new API Instance for querying /delta and initialise it
// Reuse the socket to speed up
bool keepAlive = true;
OneDriveApi getDeltaQueryOneDriveApiInstance;
getDeltaQueryOneDriveApiInstance = new OneDriveApi(appConfig);
getDeltaQueryOneDriveApiInstance.initialise(keepAlive);
for (;;) {
responseBundleCount++;
// Get the /delta changes via the OneDrive API
// getDeltaChangesByItemId has the re-try logic for transient errors
deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, currentDeltaLink, getDeltaQueryOneDriveApiInstance);
// If the initial deltaChanges response is an invalid JSON object, keep trying ..
if (deltaChanges.type() != JSONType.object) {
while (deltaChanges.type() != JSONType.object) {
// Handle the invalid JSON response adn retry
addLogEntry("ERROR: Query of the OneDrive API via deltaChanges = getDeltaChangesByItemId() returned an invalid JSON response", ["debug"]);
deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, currentDeltaLink, getDeltaQueryOneDriveApiInstance);
}
}
ulong nrChanges = count(deltaChanges["value"].array);
int changeCount = 0;
if (appConfig.verbosityCount == 0) {
// Dynamic output for a non-verbose run so that the user knows something is happening
if (!appConfig.surpressLoggingOutput) {
addLogEntry(".", ["consoleOnlyNoNewLine"]);
}
} else {
addLogEntry("Processing API Response Bundle: " ~ to!string(responseBundleCount) ~ " - Quantity of 'changes|items' in this bundle to process: " ~ to!string(nrChanges), ["verbose"]);
}
jsonItemsReceived = jsonItemsReceived + nrChanges;
// We have a valid deltaChanges JSON array. This means we have at least 200+ JSON items to process.
// The API response however cannot be run in parallel as the OneDrive API sends the JSON items in the order in which they must be processed
foreach (onedriveJSONItem; deltaChanges["value"].array) {
// increment change count for this item
changeCount++;
// Process the OneDrive object item JSON
processDeltaJSONItem(onedriveJSONItem, nrChanges, changeCount, responseBundleCount, singleDirectoryScope);
}
// The response may contain either @odata.deltaLink or @odata.nextLink
if ("@odata.deltaLink" in deltaChanges) {
// Log action
addLogEntry("Setting next currentDeltaLink to (@odata.deltaLink): " ~ deltaChanges["@odata.deltaLink"].str, ["debug"]);
// Update currentDeltaLink
currentDeltaLink = deltaChanges["@odata.deltaLink"].str;
// Store this for later use post processing jsonItemsToProcess items
latestDeltaLink = deltaChanges["@odata.deltaLink"].str;
}
// Update deltaLink to next changeSet bundle
if ("@odata.nextLink" in deltaChanges) {
// Log action
addLogEntry("Setting next currentDeltaLink & deltaLinkAvailable to (@odata.nextLink): " ~ deltaChanges["@odata.nextLink"].str, ["debug"]);
// Update currentDeltaLink
currentDeltaLink = deltaChanges["@odata.nextLink"].str;
// Update deltaLinkAvailable to next changeSet bundle to quantify how many changes we have to process
deltaLinkAvailable = deltaChanges["@odata.nextLink"].str;
// Store this for later use post processing jsonItemsToProcess items
latestDeltaLink = deltaChanges["@odata.nextLink"].str;
}
else break;
}
// To finish off the JSON processing items, this is needed to reflect this in the log
addLogEntry("------------------------------------------------------------------", ["debug"]);
// Shutdown the API
getDeltaQueryOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(getDeltaQueryOneDriveApiInstance);
// Log that we have finished querying the /delta API
if (appConfig.verbosityCount == 0) {
if (!appConfig.surpressLoggingOutput) {
// Close out the '....' being printed to the console
addLogEntry("\n", ["consoleOnlyNoNewLine"]);
}
} else {
addLogEntry("Finished processing /delta JSON response from the OneDrive API", ["verbose"]);
}
// If this was set, now unset it, as this will have been completed, so that for a true up, we dont do a double full scan
if (appConfig.fullScanTrueUpRequired) {
addLogEntry("Unsetting fullScanTrueUpRequired as this has been performed", ["debug"]);
appConfig.fullScanTrueUpRequired = false;
}
} else {
// Why are are generating a /delta response
addLogEntry("Why are we generating a /delta response:", ["debug"]);
addLogEntry(" singleDirectoryScope: " ~ to!string(singleDirectoryScope), ["debug"]);
addLogEntry(" nationalCloudDeployment: " ~ to!string(nationalCloudDeployment), ["debug"]);
addLogEntry(" cleanupLocalFiles: " ~ to!string(cleanupLocalFiles), ["debug"]);
// What 'path' are we going to start generating the response for
string pathToQuery;
// If --single-directory has been called, use the value that has been set
if (singleDirectoryScope) {
pathToQuery = appConfig.getValueString("single_directory");
}
// We could also be syncing a Shared Folder of some description
if (!sharedFolderName.empty) {
pathToQuery = sharedFolderName;
}
// Generate the simulated /delta response
//
// The generated /delta response however contains zero deleted JSON items, so the only way that we can track this, is if the object was in sync
// we have the object in the database, thus, what we need to do is for every DB object in the tree of items, flag 'syncStatus' as 'N', then when we process
// the returned JSON items from the API, we flag the item as back in sync, then we can cleanup any out-of-sync items
//
// The flagging of the local database items to 'N' is handled within the generateDeltaResponse() function
//
// When these JSON items are then processed, if the item exists online, and is in the DB, and that the values match, the DB item is flipped back to 'Y'
// This then allows the application to look for any remaining 'N' values, and delete these as no longer needed locally
deltaChanges = generateDeltaResponse(pathToQuery);
ulong nrChanges = count(deltaChanges["value"].array);
int changeCount = 0;
addLogEntry("API Response Bundle: " ~ to!string(responseBundleCount) ~ " - Quantity of 'changes|items' in this bundle to process: " ~ to!string(nrChanges), ["debug"]);
jsonItemsReceived = jsonItemsReceived + nrChanges;
// The API response however cannot be run in parallel as the OneDrive API sends the JSON items in the order in which they must be processed
foreach (onedriveJSONItem; deltaChanges["value"].array) {
// increment change count for this item
changeCount++;
// Process the OneDrive object item JSON
processDeltaJSONItem(onedriveJSONItem, nrChanges, changeCount, responseBundleCount, singleDirectoryScope);
}
// To finish off the JSON processing items, this is needed to reflect this in the log
addLogEntry("------------------------------------------------------------------", ["debug"]);
// Log that we have finished generating our self generated /delta response
if (!appConfig.surpressLoggingOutput) {
addLogEntry("Finished processing self generated /delta JSON response from the OneDrive API");
}
}
// Cleanup deltaChanges as this is no longer needed
object.destroy(deltaChanges);
// We have JSON items received from the OneDrive API
addLogEntry("Number of JSON Objects received from OneDrive API: " ~ to!string(jsonItemsReceived), ["debug"]);
addLogEntry("Number of JSON Objects already processed (root and deleted items): " ~ to!string((jsonItemsReceived - jsonItemsToProcess.length)), ["debug"]);
// We should have now at least processed all the JSON items as returned by the /delta call
// Additionally, we should have a new array, that now contains all the JSON items we need to process that are non 'root' or deleted items
addLogEntry("Number of JSON items to process is: " ~ to!string(jsonItemsToProcess.length), ["debug"]);
// Are there items to process?
if (jsonItemsToProcess.length > 0) {
// Lets deal with the JSON items in a batch process
ulong batchSize = 500;
ulong batchCount = (jsonItemsToProcess.length + batchSize - 1) / batchSize;
ulong batchesProcessed = 0;
// Dynamic output for a non-verbose run so that the user knows something is happening
if (!appConfig.surpressLoggingOutput) {
// Logfile entry
addLogEntry("Processing " ~ to!string(jsonItemsToProcess.length) ~ " applicable changes and items received from Microsoft OneDrive", ["logFileOnly"]);
// Console only output
addLogEntry("Processing " ~ to!string(jsonItemsToProcess.length) ~ " applicable changes and items received from Microsoft OneDrive ", ["consoleOnlyNoNewLine"]);
if (appConfig.verbosityCount != 0) {
// Close out the console only processing line above, if we are doing verbose or above logging
addLogEntry("\n", ["consoleOnlyNoNewLine"]);
}
}
// For each batch, process the JSON items that need to be now processed.
// 'root' and deleted objects have already been handled
foreach (batchOfJSONItems; jsonItemsToProcess.chunks(batchSize)) {
// Chunk the total items to process into 500 lot items
batchesProcessed++;
if (appConfig.verbosityCount == 0) {
// Dynamic output for a non-verbose run so that the user knows something is happening
if (!appConfig.surpressLoggingOutput) {
addLogEntry(".", ["consoleOnlyNoNewLine"]);
}
} else {
addLogEntry("Processing OneDrive JSON item batch [" ~ to!string(batchesProcessed) ~ "/" ~ to!string(batchCount) ~ "] to ensure consistent local state", ["verbose"]);
}
// Process the batch
processJSONItemsInBatch(batchOfJSONItems, batchesProcessed, batchCount);
// To finish off the JSON processing items, this is needed to reflect this in the log
addLogEntry("------------------------------------------------------------------", ["debug"]);
}
if (appConfig.verbosityCount == 0) {
// close off '.' output
if (!appConfig.surpressLoggingOutput) {
addLogEntry("\n", ["consoleOnlyNoNewLine"]);
}
}
// Free up memory and items processed as it is pointless now having this data around
jsonItemsToProcess = [];
// Debug output - what was processed
addLogEntry("Number of JSON items to process is: " ~ to!string(jsonItemsToProcess.length), ["debug"]);
addLogEntry("Number of JSON items processed was: " ~ to!string(processedCount), ["debug"]);
} else {
if (!appConfig.surpressLoggingOutput) {
addLogEntry("No additional changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive");
}
}
// Update the deltaLink in the database so that we can reuse this now that jsonItemsToProcess has been processed
if (!latestDeltaLink.empty) {
addLogEntry("Updating completed deltaLink in DB to: " ~ latestDeltaLink, ["debug"]);
itemDB.setDeltaLink(driveIdToQuery, itemIdToQuery, latestDeltaLink);
}
// Keep the driveIDsArray with unique entries only
if (!canFind(driveIDsArray, driveIdToQuery)) {
// Add this driveId to the array of driveId's we know about
driveIDsArray ~= driveIdToQuery;
}
}
// Process the /delta API JSON response items
void processDeltaJSONItem(JSONValue onedriveJSONItem, ulong nrChanges, int changeCount, ulong responseBundleCount, bool singleDirectoryScope) {
// Variables for this foreach loop
string thisItemId;
bool itemIsRoot = false;
bool handleItemAsRootObject = false;
bool itemIsDeletedOnline = false;
bool itemHasParentReferenceId = false;
bool itemHasParentReferencePath = false;
bool itemIdMatchesDefaultRootId = false;
bool itemNameExplicitMatchRoot = false;
string objectParentDriveId;
addLogEntry("------------------------------------------------------------------", ["debug"]);
addLogEntry("Processing OneDrive Item " ~ to!string(changeCount) ~ " of " ~ to!string(nrChanges) ~ " from API Response Bundle " ~ to!string(responseBundleCount), ["debug"]);
addLogEntry("Raw JSON OneDrive Item: " ~ to!string(onedriveJSONItem), ["debug"]);
// What is this item's id
thisItemId = onedriveJSONItem["id"].str;
// Is this a deleted item - only calculate this once
itemIsDeletedOnline = isItemDeleted(onedriveJSONItem);
if(!itemIsDeletedOnline){
// This is not a deleted item
addLogEntry("This item is not a OneDrive deletion change", ["debug"]);