abraunegg-onedrive/src/sync.d
abraunegg 380e3167f2 Update sync.d
* Fix that the '.' were not being printed in --verbose mode for fetching the /delta response
2023-10-19 10:05:03 +11:00

6986 lines
320 KiB
D

// 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;
// What other modules that we have created do we need to import?
import config;
import log;
import util;
import onedrive;
import itemdb;
import clientSideFiltering;
import progress;
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;
// 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")) {
log.vdebug("Configuring uploadOnly flag to TRUE as --upload-only passed in or configured");
this.uploadOnly = true;
}
// Configure the localDeleteAfterUpload flag
if (appConfig.getValueBool("remove_source_files")) {
log.vdebug("Configuring localDeleteAfterUpload flag to TRUE as --remove-source-files passed in or configured");
this.localDeleteAfterUpload = true;
}
// Configure the disableDownloadValidation flag
if (appConfig.getValueBool("disable_download_validation")) {
log.vdebug("Configuring disableDownloadValidation flag to TRUE as --disable-download-validation passed in or configured");
this.disableDownloadValidation = true;
}
// Configure the disableUploadValidation flag
if (appConfig.getValueBool("disable_upload_validation")) {
log.vdebug("Configuring disableUploadValidation flag to TRUE as --disable-upload-validation passed in or configured");
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
log.log("WARNING: Application has been configured to cleanup local files that are not present online.");
log.log("WARNING: Local data loss MAY occur in this scenario if you are expecting data to remain archived locally.");
// 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
log.log("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")) {
log.log("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")) {
log.log("WARNING: Application has been configured to bypass local data preservation in the event of file conflict.");
log.log("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
log.log("User Configured Rate Limit: ", 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
log.log("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
log.vlog("Downgrading all HTTP operations to HTTP/1.1 due to user configuration");
} else {
// Use curl defaults
log.vdebug("Using Curl defaults for HTTP operational protocol version (potentially HTTP/2)");
}
}
// 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
log.error(exception.msg);
// Shutdown API instance
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
exit(-1);
}
try {
// Get the relevant default account & drive details
getDefaultRootDetails();
} catch (accountDetailsException exception) {
// details could not be queried
log.error(exception.msg);
// Shutdown API instance
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
exit(-1);
}
try {
// Display details
displaySyncEngineDetails();
} catch (accountDetailsException exception) {
// details could not be queried
log.error(exception.msg);
// Shutdown API instance
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
exit(-1);
}
} else {
// API could not be initialised
log.error("OneDrive API could not be initialised with previously used details");
// Shutdown API instance
oneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(oneDriveApiInstance);
exit(-1);
}
log.log("Sync Engine Initialised with new Onedrive API instance");
// Shutdown API instance
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 {
log.vdebug("Getting Account Default Drive Details");
defaultOneDriveDriveDetails = oneDriveApiInstance.getDefaultDriveDetails();
} catch (OneDriveException exception) {
log.vdebug("defaultOneDriveDriveDetails = oneDriveApiInstance.getDefaultDriveDetails() generated a OneDriveException");
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);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to query Account Default Drive Details - retrying applicable request in 30 seconds");
log.vdebug("defaultOneDriveDriveDetails = oneDriveApiInstance.getDefaultDriveDetails() previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429 and 504 - but loop back calling this function
log.vdebug("Retrying Function: getDefaultDriveDetails()");
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))) {
log.vdebug("OneDrive Account Default Drive Details: ", defaultOneDriveDriveDetails);
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
log.error("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?
log.error("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") {
log.error("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
log.error("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
log.vdebug("appConfig.accountType = ", appConfig.accountType);
log.vdebug("appConfig.defaultDriveId = ", appConfig.defaultDriveId);
log.vdebug("appConfig.remainingFreeSpace = ", appConfig.remainingFreeSpace);
log.vdebug("appConfig.quotaAvailable = ", appConfig.quotaAvailable);
log.vdebug("appConfig.quotaRestricted = ", appConfig.quotaRestricted);
// 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 {
log.vdebug("Getting Account Default Root Details");
defaultOneDriveRootDetails = oneDriveApiInstance.getDefaultRootDetails();
} catch (OneDriveException exception) {
log.vdebug("defaultOneDriveRootDetails = oneDriveApiInstance.getDefaultRootDetails() generated a OneDriveException");
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);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to query Account Default Root Details - retrying applicable request in 30 seconds");
log.vdebug("defaultOneDriveRootDetails = oneDriveApiInstance.getDefaultRootDetails() previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: getDefaultRootDetails()");
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))) {
log.vdebug("OneDrive Account Default Root Details: ", defaultOneDriveRootDetails);
appConfig.defaultRootId = defaultOneDriveRootDetails["id"].str;
log.vdebug("appConfig.defaultRootId = ", appConfig.defaultRootId);
// 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)) {
log.log("Resetting syncFailures = false");
syncFailures = false;
} else {
log.log("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
log.vdebug("Perform a Full Scan True-Up: ", appConfig.fullScanTrueUpRequired);
// 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
log.vlog("Skipping item - excluded by skip_dir config: ", remoteItem.name);
continue;
}
}
// Directory name is not excluded or skip_dir is not populated
if (!appConfig.surpressLoggingOutput) {
log.log("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
log.vlog("Skipping item - excluded by skip_dir config: ", remoteItem.name);
continue;
}
}
// Directory name is not excluded or skip_dir is not populated
if (!appConfig.surpressLoggingOutput) {
log.log("Syncing this OneDrive Business Shared Folder: ", remoteItem.name);
}
log.vdebug("Fetching /delta API response for:");
log.vdebug(" remoteItem.remoteDriveId: ", remoteItem.remoteDriveId);
log.vdebug(" remoteItem.remoteId: ", remoteItem.remoteId);
// 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?
log.log("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);
log.error("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);
log.vdebug("searchItem: ", searchItem);
// 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 {
log.error("\nThe requested --single-directory path to sync has generated an error. Please correct this error and try again.\n");
exit(EXIT_FAILURE);
}
}
// 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
log.vdebug("driveIdToQuery was empty, setting to appConfig.defaultDriveId");
driveIdToQuery = appConfig.defaultDriveId;
log.vdebug("driveIdToQuery: ", driveIdToQuery);
}
// Was an itemId provided as an input
//if (itemIdToQuery == "") {
if (strip(itemIdToQuery).empty) {
// No provided itemId to query, use the account default
log.vdebug("itemIdToQuery was empty, setting to appConfig.defaultRootId");
itemIdToQuery = appConfig.defaultRootId;
log.vdebug("itemIdToQuery: ", itemIdToQuery);
}
// 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) {
log.vdebug("Using database stored deltaLink");
currentDeltaLink = deltaLinkAvailable;
}
// Do we need to perform a Full Scan True Up? Is 'appConfig.fullScanTrueUpRequired' set to 'true'?
if (appConfig.fullScanTrueUpRequired) {
log.log("Performing a full scan of online data to ensure consistent local state");
log.vdebug("Setting currentDeltaLink = null");
currentDeltaLink = null;
}
// Dynamic output for non-verbose and verbose run so that the user knows something is being retreived from the OneDrive API
if (log.verbose <= 1) {
if (!appConfig.surpressLoggingOutput) {
log.fileOnly("Fetching items from the OneDrive API for Drive ID: ", driveIdToQuery);
// Use the dots to show the application is 'doing something'
write("Fetching items from the OneDrive API for Drive ID: ", driveIdToQuery, " .");
}
} else {
log.vdebug("Fetching /delta response from the OneDrive API for Drive ID: ", driveIdToQuery);
}
// Create a new API Instance for querying /delta and initialise it
OneDriveApi getDeltaQueryOneDriveApiInstance;
getDeltaQueryOneDriveApiInstance = new OneDriveApi(appConfig);
getDeltaQueryOneDriveApiInstance.initialise();
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
log.vdebug("ERROR: Query of the OneDrive API via deltaChanges = getDeltaChangesByItemId() returned an invalid JSON response");
deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, currentDeltaLink, getDeltaQueryOneDriveApiInstance);
}
}
ulong nrChanges = count(deltaChanges["value"].array);
int changeCount = 0;
if (log.verbose <= 1) {
// Dynamic output for a non-verbose run so that the user knows something is happening
if (!appConfig.surpressLoggingOutput) {
write(".");
}
} else {
log.vdebug("API Response Bundle: ", responseBundleCount, " - Quantity of 'changes|items' in this bundle to process: ", nrChanges);
}
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
log.vdebug("Setting next currentDeltaLink to (@odata.deltaLink): ", deltaChanges["@odata.deltaLink"].str);
// 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
log.vdebug("Setting next currentDeltaLink & deltaLinkAvailable to (@odata.nextLink): ", deltaChanges["@odata.nextLink"].str);
// 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
log.vdebug("------------------------------------------------------------------");
// Shutdown the API
getDeltaQueryOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(getDeltaQueryOneDriveApiInstance);
// Log that we have finished querying the /delta API
if (log.verbose <= 1) {
if (!appConfig.surpressLoggingOutput) {
write("\n");
}
} else {
log.vdebug("Finished processing /delta JSON response from the OneDrive API");
}
// 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) {
log.vdebug("Unsetting fullScanTrueUpRequired as this has been performed");
appConfig.fullScanTrueUpRequired = false;
}
} else {
// Why are are generating a /delta response
log.vdebug("Why are we generating a /delta response:");
log.vdebug(" singleDirectoryScope: ", singleDirectoryScope);
log.vdebug(" nationalCloudDeployment: ", nationalCloudDeployment);
log.vdebug(" cleanupLocalFiles: ", cleanupLocalFiles);
// 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;
log.vdebug("API Response Bundle: ", responseBundleCount, " - Quantity of 'changes|items' in this bundle to process: ", nrChanges);
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
log.vdebug("------------------------------------------------------------------");
// Log that we have finished generating our self generated /delta response
if (!appConfig.surpressLoggingOutput) {
log.log("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
log.vdebug("Number of JSON Objects received from OneDrive API: ", jsonItemsReceived);
log.vdebug("Number of JSON Objects already processed (root and deleted items): ", (jsonItemsReceived - jsonItemsToProcess.length));
// 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
log.vdebug("Number of JSON items to process is: ", jsonItemsToProcess.length);
// 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) {
write("Processing ", jsonItemsToProcess.length, " applicable changes and items received from Microsoft OneDrive ");
log.fileOnly("Processing ", jsonItemsToProcess.length, " applicable changes and items received from Microsoft OneDrive");
if (log.verbose != 0) {
// close out the write() processing line above
writeln();
}
}
// 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 (log.verbose == 0) {
// Dynamic output for a non-verbose run so that the user knows something is happening
if (!appConfig.surpressLoggingOutput) {
write(".");
}
} else {
log.vlog("Processing OneDrive JSON item batch [", batchesProcessed,"/", batchCount, "] to ensure consistent local state");
}
// Process the batch
processJSONItemsInBatch(batchOfJSONItems, batchesProcessed, batchCount);
// To finish off the JSON processing items, this is needed to reflect this in the log
log.vdebug("------------------------------------------------------------------");
}
if (log.verbose == 0) {
// close off '.' output
if (!appConfig.surpressLoggingOutput) {
writeln();
}
}
// Free up memory and items processed as it is pointless now having this data around
jsonItemsToProcess = [];
// Debug output - what was processed
log.vdebug("Number of JSON items to process is: ", jsonItemsToProcess.length);
log.vdebug("Number of JSON items processed was: ", processedCount);
} else {
if (!appConfig.surpressLoggingOutput) {
log.log("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) {
log.vdebug("Updating completed deltaLink in DB to: ", latestDeltaLink);
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;
log.vdebug("------------------------------------------------------------------");
log.vdebug("Processing OneDrive Item ", changeCount, " of ", nrChanges, " from API Response Bundle ", responseBundleCount);
log.vdebug("Raw JSON OneDrive Item: ", onedriveJSONItem);
// 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
log.vdebug("This item is not a OneDrive deletion change");
// Only calculate this once
itemIsRoot = isItemRoot(onedriveJSONItem);
itemHasParentReferenceId = hasParentReferenceId(onedriveJSONItem);
itemIdMatchesDefaultRootId = (thisItemId == appConfig.defaultRootId);
itemNameExplicitMatchRoot = (onedriveJSONItem["name"].str == "root");
objectParentDriveId = onedriveJSONItem["parentReference"]["driveId"].str;
// Shared Folder Items
// !hasParentReferenceId(id)
// !hasParentReferenceId(path)
// Test is this is the OneDrive Users Root?
// Debug output of change evaluation items
log.vdebug("defaultRootId = ", appConfig.defaultRootId);
log.vdebug("'search id' = ", thisItemId);
log.vdebug("id == defaultRootId = ", itemIdMatchesDefaultRootId);
log.vdebug("isItemRoot(onedriveJSONItem) = ", itemIsRoot);
log.vdebug("onedriveJSONItem['name'].str == 'root' = ", itemNameExplicitMatchRoot);
log.vdebug("itemHasParentReferenceId = ", itemHasParentReferenceId);
if ( (itemIdMatchesDefaultRootId || singleDirectoryScope) && itemIsRoot && itemNameExplicitMatchRoot) {
// This IS a OneDrive Root item or should be classified as such in the case of 'singleDirectoryScope'
log.vdebug("JSON item will flagged as a 'root' item");
handleItemAsRootObject = true;
}
}
// How do we handle this JSON item from the OneDrive API?
// Is this a confirmed 'root' item, has no Parent ID, or is a Deleted Item
if (handleItemAsRootObject || !itemHasParentReferenceId || itemIsDeletedOnline){
// Is a root item, has no id in parentReference or is a OneDrive deleted item
log.vdebug("objectParentDriveId = ", objectParentDriveId);
log.vdebug("handleItemAsRootObject = ", handleItemAsRootObject);
log.vdebug("itemHasParentReferenceId = ", itemHasParentReferenceId);
log.vdebug("itemIsDeletedOnline = ", itemIsDeletedOnline);
log.vdebug("Handling change immediately as 'root item', or has no parent reference id or is a deleted item");
// OK ... do something with this JSON post here ....
processRootAndDeletedJSONItems(onedriveJSONItem, objectParentDriveId, handleItemAsRootObject, itemIsDeletedOnline, itemHasParentReferenceId);
} else {
// Do we need to update this RAW JSON from OneDrive?
if ( (objectParentDriveId != appConfig.defaultDriveId) && (appConfig.accountType == "business") && (appConfig.getValueBool("sync_business_shared_items")) ) {
// Potentially need to update this JSON data
log.vdebug("Potentially need to update this source JSON .... need to check the database");
// Check the DB for 'remote' objects, searching 'remoteDriveId' and 'remoteId' items for this remoteItem.driveId and remoteItem.id
Item remoteDBItem;
itemDB.selectByRemoteId(objectParentDriveId, thisItemId, remoteDBItem);
// Is the data that was returned from the database what we are looking for?
if ((remoteDBItem.remoteDriveId == objectParentDriveId) && (remoteDBItem.remoteId == thisItemId)) {
// Yes, this is the record we are looking for
log.vdebug("DB Item response for remoteDBItem: ", remoteDBItem);
// Must compare remoteDBItem.name with remoteItem.name
if (remoteDBItem.name != onedriveJSONItem["name"].str) {
// Update JSON Item
string actualOnlineName = onedriveJSONItem["name"].str;
log.vdebug("Updating source JSON 'name' to that which is the actual local directory");
log.vdebug("onedriveJSONItem['name'] was: ", onedriveJSONItem["name"].str);
log.vdebug("Updating onedriveJSONItem['name'] to: ", remoteDBItem.name);
onedriveJSONItem["name"] = remoteDBItem.name;
log.vdebug("onedriveJSONItem['name'] now: ", onedriveJSONItem["name"].str);
// Add the original name to the JSON
onedriveJSONItem["actualOnlineName"] = actualOnlineName;
}
}
}
// If we are not self-generating a /delta response, check this initial /delta JSON bundle item against the basic checks
// of applicability against 'skip_file', 'skip_dir' and 'sync_list'
// We only do this if we did not generate a /delta response, as generateDeltaResponse() performs the checkJSONAgainstClientSideFiltering()
// against elements as it is building the /delta compatible response
// If we blindly just 'check again' all JSON responses then there is potentially double JSON processing going on if we used generateDeltaResponse()
bool discardDeltaJSONItem = false;
if (!generateSimulatedDeltaResponse) {
// Check applicability against 'skip_file', 'skip_dir' and 'sync_list'
discardDeltaJSONItem = checkJSONAgainstClientSideFiltering(onedriveJSONItem);
}
// Add this JSON item for further processing if this is not being discarded
if (!discardDeltaJSONItem) {
log.vdebug("Adding this Raw JSON OneDrive Item to jsonItemsToProcess array for further processing");
jsonItemsToProcess ~= onedriveJSONItem;
}
}
}
// Process 'root' and 'deleted' OneDrive JSON items
void processRootAndDeletedJSONItems(JSONValue onedriveJSONItem, string driveId, bool handleItemAsRootObject, bool itemIsDeletedOnline, bool itemHasParentReferenceId) {
// Use the JSON elements rather can computing a DB struct via makeItem()
string thisItemId = onedriveJSONItem["id"].str;
string thisItemDriveId = onedriveJSONItem["parentReference"]["driveId"].str;
// Check if the item has been seen before
Item existingDatabaseItem;
bool existingDBEntry = itemDB.selectById(thisItemDriveId, thisItemId, existingDatabaseItem);
// Is the item deleted online?
if(!itemIsDeletedOnline) {
// Is the item a confirmed root object?
// The JSON item should be considered a 'root' item if:
// 1. Contains a ["root"] element
// 2. Has no ["parentReference"]["id"] ... #323 & #324 highlighted that this is false as some 'root' shared objects now can have an 'id' element .. OneDrive API change
// 2. Has no ["parentReference"]["path"]
// 3. Was detected by an input flag as to be handled as a root item regardless of actual status
if ((handleItemAsRootObject) || (!itemHasParentReferenceId)) {
log.vdebug("Handing JSON object as OneDrive 'root' object");
if (!existingDBEntry) {
// we have not seen this item before
saveItem(onedriveJSONItem);
}
}
} else {
// Change is to delete an item
log.vdebug("Handing a OneDrive Deleted Item");
if (existingDBEntry) {
// Flag to delete
log.vdebug("Flagging to delete item locally: ", onedriveJSONItem);
idsToDelete ~= [thisItemDriveId, thisItemId];
} else {
// Flag to ignore
log.vdebug("Flagging item to skip: ", onedriveJSONItem);
skippedItems.insert(thisItemId);
}
}
}
// Process each of the elements contained in jsonItemsToProcess[]
void processJSONItemsInBatch(JSONValue[] array, ulong batchGroup, ulong batchCount) {
ulong batchElementCount = array.length;
foreach (i, onedriveJSONItem; array.enumerate) {
// Use the JSON elements rather can computing a DB struct via makeItem()
ulong elementCount = i +1;
// To show this is the processing for this particular item, start off with this breaker line
log.vdebug("------------------------------------------------------------------");
log.vdebug("Processing OneDrive JSON item ", elementCount, " of ", batchElementCount, " as part of JSON Item Batch ", batchGroup, " of ", batchCount);
log.vdebug("Raw JSON OneDrive Item: ", onedriveJSONItem);
string thisItemId = onedriveJSONItem["id"].str;
string thisItemDriveId = onedriveJSONItem["parentReference"]["driveId"].str;
string thisItemParentId = onedriveJSONItem["parentReference"]["id"].str;
string thisItemName = onedriveJSONItem["name"].str;
// Create an empty item struct for an existing DB item
Item existingDatabaseItem;
// Do we NOT want this item?
bool unwanted = false; // meaning by default we will WANT this item
// Is this parent is in the database
bool parentInDatabase = false;
// What is the path of the new item
string newItemPath;
// Configure the remoteItem - so if it is used, it can be utilised later
Item remoteItem;
// Check the database for an existing entry for this JSON item
bool existingDBEntry = itemDB.selectById(thisItemDriveId, thisItemId, existingDatabaseItem);
// Calculate if the Parent Item is in the database so that it can be re-used
parentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId);
// Calculate the path of this JSON item, but we can only do this if the parent is in the database
if (parentInDatabase) {
// Calculate this items path
newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName;
log.vdebug("New Item calculated full path is: ", newItemPath);
} else {
// Parent not in the database
// Is the parent a 'folder' from another user? ie - is this a 'shared folder' that has been shared with us?
log.vdebug("Parent ID is not in DB .. ");
// Why?
if (thisItemDriveId == appConfig.defaultDriveId) {
// Flagging as unwanted
log.vdebug("Flagging as unwanted: thisItemDriveId (", thisItemDriveId,"), thisItemParentId (", thisItemParentId,") not in local database");
if (thisItemParentId in skippedItems) {
log.vdebug("Reason: thisItemParentId listed within skippedItems");
}
unwanted = true;
} else {
// Edge case as the parent (from another users OneDrive account) will never be in the database - potentially a shared object?
log.vdebug("Potential Shared Object Item: ", onedriveJSONItem);
// Format the OneDrive change into a consumable object for the database
remoteItem = makeItem(onedriveJSONItem);
log.vdebug("The reported parentId is not in the database. This potentially is a shared folder as 'remoteItem.driveId' != 'appConfig.defaultDriveId'. Relevant Details: remoteItem.driveId (", remoteItem.driveId,"), remoteItem.parentId (", remoteItem.parentId,")");
if (appConfig.accountType == "personal") {
// Personal Account Handling
// Ensure that this item has no parent
log.vdebug("Setting remoteItem.parentId to be null");
remoteItem.parentId = null;
// Add this record to the local database
log.vdebug("Update/Insert local database with remoteItem details with remoteItem.parentId as null: ", remoteItem);
itemDB.upsert(remoteItem);
} else {
// Business or SharePoint Account Handling
log.vdebug("Handling a Business or SharePoint Shared Item JSON object");
if (appConfig.accountType == "business") {
// Create a DB Tie Record for this parent object
Item parentItem;
parentItem.driveId = onedriveJSONItem["parentReference"]["driveId"].str;
parentItem.id = onedriveJSONItem["parentReference"]["id"].str;
parentItem.name = "root";
parentItem.type = ItemType.dir;
parentItem.mtime = remoteItem.mtime;
parentItem.parentId = null;
// Add this parent record to the local database
log.vdebug("Insert local database with remoteItem parent details: ", parentItem);
itemDB.upsert(parentItem);
// Ensure that this item has no parent
log.vdebug("Setting remoteItem.parentId to be null");
remoteItem.parentId = null;
// Check the DB for 'remote' objects, searching 'remoteDriveId' and 'remoteId' items for this remoteItem.driveId and remoteItem.id
Item remoteDBItem;
itemDB.selectByRemoteId(remoteItem.driveId, remoteItem.id, remoteDBItem);
// Must compare remoteDBItem.name with remoteItem.name
if ((!remoteDBItem.name.empty) && (remoteDBItem.name != remoteItem.name)) {
// Update DB Item
log.vdebug("The shared item stored in OneDrive, has a different name to the actual name on the remote drive");
log.vdebug("Updating remoteItem.name JSON data with the actual name being used on account drive and local folder");
log.vdebug("remoteItem.name was: ", remoteItem.name);
log.vdebug("Updating remoteItem.name to: ", remoteDBItem.name);
remoteItem.name = remoteDBItem.name;
log.vdebug("Setting remoteItem.remoteName to: ", onedriveJSONItem["name"].str);
// Update JSON Item
remoteItem.remoteName = onedriveJSONItem["name"].str;
log.vdebug("Updating source JSON 'name' to that which is the actual local directory");
log.vdebug("onedriveJSONItem['name'] was: ", onedriveJSONItem["name"].str);
log.vdebug("Updating onedriveJSONItem['name'] to: ", remoteDBItem.name);
onedriveJSONItem["name"] = remoteDBItem.name;
log.vdebug("onedriveJSONItem['name'] now: ", onedriveJSONItem["name"].str);
// Update newItemPath value
newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ remoteDBItem.name;
log.vdebug("New Item updated calculated full path is: ", newItemPath);
}
// Add this record to the local database
log.vdebug("Update/Insert local database with remoteItem details: ", remoteItem);
itemDB.upsert(remoteItem);
}
}
}
}
// Check the skippedItems array for the parent id of this JSONItem if this is something we need to skip
if (!unwanted) {
if (thisItemParentId in skippedItems) {
// Flag this JSON item as unwanted
log.vdebug("Flagging as unwanted: find(thisItemParentId).length != 0");
unwanted = true;
// Is this item id in the database?
if (existingDBEntry) {
// item exists in database, most likely moved out of scope for current client configuration
log.vdebug("This item was previously synced / seen by the client");
if (("name" in onedriveJSONItem["parentReference"]) != null) {
// How is this out of scope?
// is sync_list configured
if (syncListConfigured) {
// sync_list configured and in use
if (selectiveSync.isPathExcludedViaSyncList(onedriveJSONItem["parentReference"]["name"].str)) {
// Previously synced item is now out of scope as it has been moved out of what is included in sync_list
log.vdebug("This previously synced item is now excluded from being synced due to sync_list exclusion");
}
}
// flag to delete local file as it now is no longer in sync with OneDrive
log.vdebug("Flagging to delete item locally: ", onedriveJSONItem);
idsToDelete ~= [thisItemDriveId, thisItemId];
}
}
}
}
// Check the item type - if it not an item type that we support, we cant process the JSON item
if (!unwanted) {
if (isItemFile(onedriveJSONItem)) {
log.vdebug("The item we are syncing is a file");
} else if (isItemFolder(onedriveJSONItem)) {
log.vdebug("The item we are syncing is a folder");
} else if (isItemRemote(onedriveJSONItem)) {
log.vdebug("The item we are syncing is a remote item");
} else {
// Why was this unwanted?
if (newItemPath.empty) {
// Compute this item path & need the full path for this file
newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName;
log.vdebug("New Item calculated full path is: ", newItemPath);
}
// Microsoft OneNote container objects present as neither folder or file but has file size
if ((!isItemFile(onedriveJSONItem)) && (!isItemFolder(onedriveJSONItem)) && (hasFileSize(onedriveJSONItem))) {
// Log that this was skipped as this was a Microsoft OneNote item and unsupported
log.vlog("The Microsoft OneNote Notebook '", newItemPath, "' is not supported by this client");
} else {
// Log that this item was skipped as unsupported
log.vlog("The OneDrive item '", newItemPath, "' is not supported by this client");
}
unwanted = true;
log.vdebug("Flagging as unwanted: item type is not supported");
}
}
// Check if this is excluded by config option: skip_dir
if (!unwanted) {
// Only check path if config is != ""
if (!appConfig.getValueString("skip_dir").empty) {
// Is the item a folder?
if (isItemFolder(onedriveJSONItem)) {
// work out the 'snippet' path where this folder would be created
string simplePathToCheck = "";
string complexPathToCheck = "";
string matchDisplay = "";
if (hasParentReference(onedriveJSONItem)) {
// we need to workout the FULL path for this item
// simple path
if (("name" in onedriveJSONItem["parentReference"]) != null) {
simplePathToCheck = onedriveJSONItem["parentReference"]["name"].str ~ "/" ~ onedriveJSONItem["name"].str;
} else {
simplePathToCheck = onedriveJSONItem["name"].str;
}
log.vdebug("skip_dir path to check (simple): ", simplePathToCheck);
// complex path
if (parentInDatabase) {
// build up complexPathToCheck
complexPathToCheck = buildNormalizedPath(newItemPath);
} else {
log.vdebug("Parent details not in database - unable to compute complex path to check");
}
if (!complexPathToCheck.empty) {
log.vdebug("skip_dir path to check (complex): ", complexPathToCheck);
}
} else {
simplePathToCheck = onedriveJSONItem["name"].str;
}
// If 'simplePathToCheck' or 'complexPathToCheck' is of the following format: root:/folder
// then isDirNameExcluded matching will not work
// Clean up 'root:' if present
if (startsWith(simplePathToCheck, "root:")){
log.vdebug("Updating simplePathToCheck to remove 'root:'");
simplePathToCheck = strip(simplePathToCheck, "root:");
}
if (startsWith(complexPathToCheck, "root:")){
log.vdebug("Updating complexPathToCheck to remove 'root:'");
complexPathToCheck = strip(complexPathToCheck, "root:");
}
// OK .. what checks are we doing?
if ((!simplePathToCheck.empty) && (complexPathToCheck.empty)) {
// just a simple check
log.vdebug("Performing a simple check only");
unwanted = selectiveSync.isDirNameExcluded(simplePathToCheck);
} else {
// simple and complex
log.vdebug("Performing a simple then complex path match if required");
// simple first
log.vdebug("Performing a simple check first");
unwanted = selectiveSync.isDirNameExcluded(simplePathToCheck);
matchDisplay = simplePathToCheck;
if (!unwanted) {
log.vdebug("Simple match was false, attempting complex match");
// simple didnt match, perform a complex check
unwanted = selectiveSync.isDirNameExcluded(complexPathToCheck);
matchDisplay = complexPathToCheck;
}
}
// result
log.vdebug("skip_dir exclude result (directory based): ", unwanted);
if (unwanted) {
// This path should be skipped
log.vlog("Skipping item - excluded by skip_dir config: ", matchDisplay);
}
}
// Is the item a file?
// We need to check to see if this files path is excluded as well
if (isItemFile(onedriveJSONItem)) {
string pathToCheck;
// does the newItemPath start with '/'?
if (!startsWith(newItemPath, "/")){
// path does not start with '/', but we need to check skip_dir entries with and without '/'
// so always make sure we are checking a path with '/'
pathToCheck = '/' ~ dirName(newItemPath);
} else {
pathToCheck = dirName(newItemPath);
}
// perform the check
unwanted = selectiveSync.isDirNameExcluded(pathToCheck);
// result
log.vdebug("skip_dir exclude result (file based): ", unwanted);
if (unwanted) {
// this files path should be skipped
log.vlog("Skipping item - file path is excluded by skip_dir config: ", newItemPath);
}
}
}
}
// Check if this is excluded by config option: skip_file
if (!unwanted) {
// Is the JSON item a file?
if (isItemFile(onedriveJSONItem)) {
// skip_file can contain 4 types of entries:
// - wildcard - *.txt
// - text + wildcard - name*.txt
// - full path + combination of any above two - /path/name*.txt
// - full path to file - /path/to/file.txt
// is the parent id in the database?
if (parentInDatabase) {
// Compute this item path & need the full path for this file
if (newItemPath.empty) {
newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName;
log.vdebug("New Item calculated full path is: ", newItemPath);
}
// The path that needs to be checked needs to include the '/'
// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched
// However, as 'path' used throughout, use a temp variable with this modification so that we use the temp variable for exclusion checks
string exclusionTestPath = "";
if (!startsWith(newItemPath, "/")){
// Add '/' to the path
exclusionTestPath = '/' ~ newItemPath;
}
log.vdebug("skip_file item to check: ", exclusionTestPath);
unwanted = selectiveSync.isFileNameExcluded(exclusionTestPath);
log.vdebug("Result: ", unwanted);
if (unwanted) log.vlog("Skipping item - excluded by skip_file config: ", thisItemName);
} else {
// parent id is not in the database
unwanted = true;
log.vlog("Skipping file - parent path not present in local database");
}
}
}
// Check if this is included or excluded by use of sync_list
if (!unwanted) {
// No need to try and process something against a sync_list if it has been configured
if (syncListConfigured) {
// Compute the item path if empty - as to check sync_list we need an actual path to check
if (newItemPath.empty) {
// Calculate this items path
newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName;
log.vdebug("New Item calculated full path is: ", newItemPath);
}
// What path are we checking?
log.vdebug("sync_list item to check: ", newItemPath);
// Unfortunatly there is no avoiding this call to check if the path is excluded|included via sync_list
if (selectiveSync.isPathExcludedViaSyncList(newItemPath)) {
// selective sync advised to skip, however is this a file and are we configured to upload / download files in the root?
if ((isItemFile(onedriveJSONItem)) && (appConfig.getValueBool("sync_root_files")) && (rootName(newItemPath) == "") ) {
// This is a file
// We are configured to sync all files in the root
// This is a file in the logical root
unwanted = false;
} else {
// path is unwanted
unwanted = true;
log.vlog("Skipping item - excluded by sync_list config: ", newItemPath);
// flagging to skip this item now, but does this exist in the DB thus needs to be removed / deleted?
if (existingDBEntry) {
// flag to delete
log.vlog("Flagging item for local delete as item exists in database: ", newItemPath);
idsToDelete ~= [thisItemDriveId, thisItemId];
}
}
}
}
}
// Check if the user has configured to skip downloading .files or .folders: skip_dotfiles
if (!unwanted) {
if (appConfig.getValueBool("skip_dotfiles")) {
if (isDotFile(newItemPath)) {
log.vlog("Skipping item - .file or .folder: ", newItemPath);
unwanted = true;
}
}
}
// Check if this should be skipped due to a --check-for-nosync directive (.nosync)?
if (!unwanted) {
if (appConfig.getValueBool("check_nosync")) {
// need the parent path for this object
string parentPath = dirName(newItemPath);
// Check for the presence of a .nosync in the parent path
if (exists(parentPath ~ "/.nosync")) {
log.vlog("Skipping downloading item - .nosync found in parent folder & --check-for-nosync is enabled: ", newItemPath);
unwanted = true;
}
}
}
// Check if this is excluded by a user set maximum filesize to download
if (!unwanted) {
if (isItemFile(onedriveJSONItem)) {
if (fileSizeLimit != 0) {
if (onedriveJSONItem["size"].integer >= fileSizeLimit) {
log.vlog("Skipping item - excluded by skip_size config: ", thisItemName, " (", onedriveJSONItem["size"].integer/2^^20, " MB)");
}
}
}
}
// At this point all the applicable checks on this JSON object from OneDrive are complete:
// - skip_file
// - skip_dir
// - sync_list
// - skip_dotfiles
// - check_nosync
// - skip_size
// - We know if this item exists in the DB or not in the DB
// We know if this JSON item is unwanted or not
if (unwanted) {
// This JSON item is NOT wanted - it is excluded
log.vdebug("Skipping OneDrive change as this is determined to be unwanted");
// Add to the skippedItems array, but only if it is a directory ... pointless adding 'files' here, as it is the 'id' we check as the parent path which can only be a directory
if (!isItemFile(onedriveJSONItem)) {
skippedItems.insert(thisItemId);
}
} else {
// This JSON item is wanted - we need to process this JSON item further
// Take the JSON item and create a consumable object for eventual database insertion
Item newDatabaseItem = makeItem(onedriveJSONItem);
if (existingDBEntry) {
// The details of this JSON item are already in the DB
// Is the item in the DB the same as the JSON data provided - or is the JSON data advising this is an updated file?
log.vdebug("OneDrive change is an update to an existing local item");
// Compute the existing item path
// NOTE:
// string existingItemPath = computeItemPath(existingDatabaseItem.driveId, existingDatabaseItem.id);
//
// This will calculate the path as follows:
//
// existingItemPath: Document.txt
//
// Whereas above we use the following
//
// newItemPath = computeItemPath(newDatabaseItem.driveId, newDatabaseItem.parentId) ~ "/" ~ newDatabaseItem.name;
//
// Which generates the following path:
//
// changedItemPath: ./Document.txt
//
// Need to be consistent here with how 'newItemPath' was calculated
string existingItemPath = computeItemPath(existingDatabaseItem.driveId, existingDatabaseItem.parentId) ~ "/" ~ existingDatabaseItem.name;
// Attempt to apply this changed item
applyPotentiallyChangedItem(existingDatabaseItem, existingItemPath, newDatabaseItem, newItemPath, onedriveJSONItem);
} else {
// Action this JSON item as a new item as we have no DB record of it
// The actual item may actually exist locally already, meaning that just the database is out-of-date or missing the data due to --resync
// But we also cannot compute the newItemPath as the parental objects may not exist as well
log.vdebug("OneDrive change is potentially a new local item");
// Attempt to apply this potentially new item
applyPotentiallyNewLocalItem(newDatabaseItem, onedriveJSONItem, newItemPath);
}
}
// Tracking as to if this item was processed
processedCount++;
}
}
// Perform the download of any required objects in parallel
void processDownloadActivities() {
// Are there any items to delete locally? Cleanup space locally first
if (!idsToDelete.empty) {
// There are elements that potentially need to be deleted locally
log.vlog("Items to potentially delete locally: ", idsToDelete.length);
if (appConfig.getValueBool("download_only")) {
// Download only has been configured
if (cleanupLocalFiles) {
// Process online deleted items
log.vlog("Processing local deletion activity as --download-only & --cleanup-local-files configured");
processDeleteItems();
} else {
// Not cleaning up local files
log.vlog("Skipping local deletion activity as --download-only has been used");
}
} else {
// Not using --download-only process normally
processDeleteItems();
}
// Cleanup array memory
idsToDelete = [];
}
// Are there any items to download post fetching and processing the /delta data?
if (!fileJSONItemsToDownload.empty) {
// There are elements to download
log.vlog("Number of items to download from OneDrive: ", fileJSONItemsToDownload.length);
downloadOneDriveItems();
// Cleanup array memory
fileJSONItemsToDownload = [];
}
// Are there any skipped items still?
if (!skippedItems.empty) {
// Cleanup array memory
skippedItems.clear();
}
}
// If the JSON item is not in the database, it is potentially a new item that we need to action
void applyPotentiallyNewLocalItem(Item newDatabaseItem, JSONValue onedriveJSONItem, string newItemPath) {
// The JSON and Database items being passed in here have passed the following checks:
// - skip_file
// - skip_dir
// - sync_list
// - skip_dotfiles
// - check_nosync
// - skip_size
// - Is not currently cached in the local database
// As such, we should not be doing any other checks here to determine if the JSON item is wanted .. it is
if (exists(newItemPath)) {
// Issue #2209 fix - test if path is a bad symbolic link
if (isSymlink(newItemPath)) {
log.vdebug("Path on local disk is a symbolic link ........");
if (!exists(readLink(newItemPath))) {
// reading the symbolic link failed
log.vdebug("Reading the symbolic link target failed ........ ");
log.logAndNotify("Skipping item - invalid symbolic link: ", newItemPath);
return;
}
}
// Path exists locally, is not a bad symbolic link
// Test if this item is actually in-sync
// What is the source of this item data?
string itemSource = "remote";
if (isItemSynced(newDatabaseItem, newItemPath, itemSource)) {
// Item details from OneDrive and local item details in database are in-sync
log.vdebug("The item to sync is already present on the local filesystem and is in-sync with what is reported online");
log.vdebug("Update/Insert local database with item details");
log.vdebug("item details to update/insert: ", newDatabaseItem);
itemDB.upsert(newDatabaseItem);
return;
} else {
// Item details from OneDrive and local item details in database are NOT in-sync
log.vdebug("The item to sync exists locally but is NOT in the local database - otherwise this would be handled as changed item");
// Which object is newer? The local file or the remote file?
SysTime localModifiedTime = timeLastModified(newItemPath).toUTC();
SysTime itemModifiedTime = newDatabaseItem.mtime;
// Reduce time resolution to seconds before comparing
localModifiedTime.fracSecs = Duration.zero;
itemModifiedTime.fracSecs = Duration.zero;
// If we need to rename the file, what do we rename it to?
auto ext = extension(newItemPath);
auto renamedNewItemPath = newItemPath.chomp(ext) ~ "-" ~ deviceName ~ ext;
// Is the local modified time greater than that from OneDrive?
if (localModifiedTime > itemModifiedTime) {
// Local file is newer than item on OneDrive based on file modified time
// Is this item id in the database?
if (itemDB.idInLocalDatabase(newDatabaseItem.driveId, newDatabaseItem.id)) {
// item id is in the database
// no local rename
// no download needed
log.vlog("Local item modified time is newer based on UTC time conversion - keeping local item as this exists in the local database");
log.vdebug("Skipping OneDrive change as this is determined to be unwanted due to local item modified time being newer than OneDrive item and present in the sqlite database");
} else {
// item id is not in the database .. maybe a --resync ?
// file exists locally but is not in the sqlite database - maybe a failed download?
log.vlog("Local item does not exist in local database - replacing with file from OneDrive - failed download?");
// In a --resync scenario or if items.sqlite3 was deleted before startup we have zero way of knowing IF the local file is meant to be the right file
// To this pint we have passed the following checks:
// 1. Any client side filtering checks - this determined this is a file that is wanted
// 2. A file with the exact name exists locally
// 3. The local modified time > remote modified time
// 4. The id of the item from OneDrive is not in the database
// Has the user configured to IGNORE local data protection rules?
if (bypassDataPreservation) {
// The user has configured to ignore data safety checks and overwrite local data rather than preserve & rename
log.vlog("WARNING: Local Data Protection has been disabled. You may experience data loss on this file: ", newItemPath);
} else {
// local data protection is configured, renaming local file
log.log("The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent local data loss: ", newItemPath, " -> ", renamedNewItemPath);
// perform the rename action of the local file
if (!dryRun) {
// Perform the local rename of the existing local file
safeRename(newItemPath, renamedNewItemPath, dryRun);
} else {
// Expectation here is that there is a new file locally (renamedNewItemPath) however as we don't create this, the "new file" will not be uploaded as it does not exist
log.vdebug("DRY-RUN: Skipping local file rename");
}
}
}
} else {
// Remote file is newer than the existing local item
log.vlog("Remote item modified time is newer based on UTC time conversion"); // correct message, remote item is newer
log.vdebug("localModifiedTime (local file): ", localModifiedTime);
log.vdebug("itemModifiedTime (OneDrive item): ", itemModifiedTime);
// Has the user configured to IGNORE local data protection rules?
if (bypassDataPreservation) {
// The user has configured to ignore data safety checks and overwrite local data rather than preserve & rename
log.vlog("WARNING: Local Data Protection has been disabled. You may experience data loss on this file: ", newItemPath);
} else {
// local data protection is configured, renaming local file
log.vlog("The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent data loss: ", newItemPath, " -> ", renamedNewItemPath);
// perform the rename action of the local file
if (!dryRun) {
// Perform the local rename of the existing local file
safeRename(newItemPath, renamedNewItemPath, dryRun);
} else {
// Expectation here is that there is a new file locally (renamedNewItemPath) however as we don't create this, the "new file" will not be uploaded as it does not exist
log.vdebug("DRY-RUN: Skipping local file rename");
}
}
}
}
}
// Path does not exist locally (should not exist locally if renamed file) - this will be a new file download or new folder creation
// How to handle this Potentially New Local Item JSON ?
final switch (newDatabaseItem.type) {
case ItemType.file:
// Add to the items to download array for processing
fileJSONItemsToDownload ~= onedriveJSONItem;
break;
case ItemType.dir:
case ItemType.remote:
log.log("Creating local directory: ", newItemPath);
if (!dryRun) {
try {
// Create the new directory
log.vdebug("Requested path does not exist, creating directory structure: ", newItemPath);
mkdirRecurse(newItemPath);
// Configure the applicable permissions for the folder
log.vdebug("Setting directory permissions for: ", newItemPath);
newItemPath.setAttributes(appConfig.returnRequiredDirectoryPermisions());
// Update the time of the folder to match the last modified time as is provided by OneDrive
// If there are any files then downloaded into this folder, the last modified time will get
// updated by the local Operating System with the latest timestamp - as this is normal operation
// as the directory has been modified
log.vdebug("Setting directory lastModifiedDateTime for: ", newItemPath , " to ", newDatabaseItem.mtime);
log.vdebug("Calling setTimes() for this file: ", newItemPath);
setTimes(newItemPath, newDatabaseItem.mtime, newDatabaseItem.mtime);
// Save the item to the database
saveItem(onedriveJSONItem);
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
} else {
// we dont create the directory, but we need to track that we 'faked it'
idsFaked ~= [newDatabaseItem.driveId, newDatabaseItem.id];
// Save the item to the dry-run database
saveItem(onedriveJSONItem);
}
break;
case ItemType.unknown:
// Unknown type - we dont action or sync these items
break;
}
}
// If the JSON item IS in the database, this will be an update to an existing in-sync item
void applyPotentiallyChangedItem(Item existingDatabaseItem, string existingItemPath, Item changedOneDriveItem, string changedItemPath, JSONValue onedriveJSONItem) {
// If we are moving the item, we do not need to download it again
bool itemWasMoved = false;
// Do we need to actually update the database with the details that were provided by the OneDrive API?
// Calculate these time items from the provided items
SysTime existingItemModifiedTime = existingDatabaseItem.mtime;
existingItemModifiedTime.fracSecs = Duration.zero;
SysTime changedOneDriveItemModifiedTime = changedOneDriveItem.mtime;
changedOneDriveItemModifiedTime.fracSecs = Duration.zero;
if (existingDatabaseItem.eTag != changedOneDriveItem.eTag) {
// The eTag has changed to what we previously cached
if (existingItemPath != changedItemPath) {
// Log that we are changing / moving an item to a new name
log.log("Moving ", existingItemPath, " to ", changedItemPath);
// Is the destination path empty .. or does something exist at that location?
if (exists(changedItemPath)) {
// Destination we are moving to exists ...
Item changedLocalItem;
// Query DB for this changed item in specified path that exists and see if it is in-sync
if (itemDB.selectByPath(changedItemPath, changedOneDriveItem.driveId, changedLocalItem)) {
// The 'changedItemPath' is in the database
string itemSource = "database";
if (isItemSynced(changedLocalItem, changedItemPath, itemSource)) {
// The destination item is in-sync
log.vlog("Destination is in sync and will be overwritten");
} else {
// The destination item is different
log.vlog("The destination is occupied with a different item, renaming the conflicting file...");
// Backup this item, passing in if we are performing a --dry-run or not
safeBackup(changedItemPath, dryRun);
}
} else {
// The to be overwritten item is not already in the itemdb, so it should saved to avoid data loss
log.vlog("The destination is occupied by an existing un-synced file, renaming the conflicting file...");
// Backup this item, passing in if we are performing a --dry-run or not
safeBackup(changedItemPath, dryRun);
}
}
// Try and rename path, catch any exception generated
try {
// Rename this item, passing in if we are performing a --dry-run or not
safeRename(existingItemPath, changedItemPath, dryRun);
// If the item is a file, make sure that the local timestamp now is the same as the timestamp online
// Otherwise when we do the DB check, the move on the file system, the file technically has a newer timestamp
// which is 'correct' .. but we need to report locally the online timestamp here as the move was made online
if (changedOneDriveItem.type == ItemType.file) {
setTimes(changedItemPath, changedOneDriveItem.mtime, changedOneDriveItem.mtime);
}
// Flag that the item was moved | renamed
itemWasMoved = true;
// If we are in a --dry-run situation, the actual rename did not occur - but we need to track like it did
if (dryRun) {
// Track this as a faked id item
idsFaked ~= [changedOneDriveItem.driveId, changedOneDriveItem.id];
// We also need to track that we did not rename this path
pathsRenamed ~= [existingItemPath];
}
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
}
// What sort of changed item is this?
// Is it a file, and we did not move it ..
if ((changedOneDriveItem.type == ItemType.file) && (!itemWasMoved)) {
// The eTag is notorious for being 'changed' online by some backend Microsoft process
if (existingDatabaseItem.quickXorHash != changedOneDriveItem.quickXorHash) {
// Add to the items to download array for processing - the file hash we previously recorded is not the same as online
fileJSONItemsToDownload ~= onedriveJSONItem;
} else {
// If the timestamp is different, or we are running a client operational mode that does not support /delta queries - we have to update the DB with the details from OneDrive
// Unfortunatly because of the consequence of Nataional Cloud Deployments not supporting /delta queries, the application uses the local database to flag what is out-of-date / track changes
// This means that the constant disk writing to the database fix implemented with https://github.com/abraunegg/onedrive/pull/2004 cannot be utilised when using these operational modes
// as all records are touched / updated when performing the OneDrive sync operations. The impacted operational modes are:
// - National Cloud Deployments do not support /delta as a query
// - When using --single-directory
// - When using --download-only --cleanup-local-files
// Is the last modified timestamp in the DB the same as the API data or are we running an operational mode where we simulated the /delta response?
if ((existingItemModifiedTime != changedOneDriveItemModifiedTime) || (generateSimulatedDeltaResponse)) {
// Save this item in the database
// Add to the local database
log.vdebug("Adding changed OneDrive Item to database: ", changedOneDriveItem);
itemDB.upsert(changedOneDriveItem);
}
}
} else {
// Save this item in the database
saveItem(onedriveJSONItem);
// If the 'Add shortcut to My files' link was the item that was actually renamed .. we have to update our DB records
if (changedOneDriveItem.type == ItemType.remote) {
// Select remote item data from the database
Item existingRemoteDbItem;
itemDB.selectById(changedOneDriveItem.remoteDriveId, changedOneDriveItem.remoteId, existingRemoteDbItem);
// Update the 'name' in existingRemoteDbItem and save it back to the database
// This is the local name stored on disk that was just 'moved'
existingRemoteDbItem.name = changedOneDriveItem.name;
itemDB.upsert(existingRemoteDbItem);
}
}
} else {
// The existingDatabaseItem.eTag == changedOneDriveItem.eTag .. nothing has changed eTag wise
// If the timestamp is different, or we are running a client operational mode that does not support /delta queries - we have to update the DB with the details from OneDrive
// Unfortunatly because of the consequence of Nataional Cloud Deployments not supporting /delta queries, the application uses the local database to flag what is out-of-date / track changes
// This means that the constant disk writing to the database fix implemented with https://github.com/abraunegg/onedrive/pull/2004 cannot be utilised when using these operational modes
// as all records are touched / updated when performing the OneDrive sync operations. The impacted operational modes are:
// - National Cloud Deployments do not support /delta as a query
// - When using --single-directory
// - When using --download-only --cleanup-local-files
// Is the last modified timestamp in the DB the same as the API data or are we running an operational mode where we simulated the /delta response?
if ((existingItemModifiedTime != changedOneDriveItemModifiedTime) || (generateSimulatedDeltaResponse)) {
// Database update needed for this item because our local record is out-of-date
// Add to the local database
log.vdebug("Adding changed OneDrive Item to database: ", changedOneDriveItem);
itemDB.upsert(changedOneDriveItem);
}
}
}
// Download new file items as identified
void downloadOneDriveItems() {
// Lets deal with all the JSON items that need to be downloaded in a batch process
ulong batchSize = appConfig.concurrentThreads;
ulong batchCount = (fileJSONItemsToDownload.length + batchSize - 1) / batchSize;
ulong batchesProcessed = 0;
foreach (chunk; fileJSONItemsToDownload.chunks(batchSize)) {
// send an array containing 'appConfig.concurrentThreads' (16) JSON items to download
downloadOneDriveItemsInParallel(chunk);
}
}
// Download items in parallel
void downloadOneDriveItemsInParallel(JSONValue[] array) {
// This function recieved an array of 16 JSON items to download
foreach (i, onedriveJSONItem; taskPool.parallel(array)) {
// Take each JSON item and
downloadFileItem(onedriveJSONItem);
}
}
// Perform the actual download of an object from OneDrive
void downloadFileItem(JSONValue onedriveJSONItem) {
bool downloadFailed = false;
string OneDriveFileXORHash;
string OneDriveFileSHA256Hash;
ulong jsonFileSize = 0;
// Download item specifics
string downloadDriveId = onedriveJSONItem["parentReference"]["driveId"].str;
string downloadParentId = onedriveJSONItem["parentReference"]["id"].str;
string downloadItemName = onedriveJSONItem["name"].str;
string downloadItemId = onedriveJSONItem["id"].str;
// Calculate this items path
string newItemPath = computeItemPath(downloadDriveId, downloadParentId) ~ "/" ~ downloadItemName;
log.vdebug("New Item calculated full path is: ", newItemPath);
// Is the item reported as Malware ?
if (isMalware(onedriveJSONItem)){
// OneDrive reports that this file is malware
log.error("ERROR: MALWARE DETECTED IN FILE - DOWNLOAD SKIPPED: ", newItemPath);
downloadFailed = true;
} else {
// Grab this file's filesize
if (hasFileSize(onedriveJSONItem)) {
// Use the configured filesize as reported by OneDrive
jsonFileSize = onedriveJSONItem["size"].integer;
} else {
// filesize missing
log.vdebug("WARNING: onedriveJSONItem['size'] is missing");
}
// Configure the hashes for comparison post download
if (hasHashes(onedriveJSONItem)) {
// File details returned hash details
// QuickXorHash
if (hasQuickXorHash(onedriveJSONItem)) {
// Use the provided quickXorHash as reported by OneDrive
if (onedriveJSONItem["file"]["hashes"]["quickXorHash"].str != "") {
OneDriveFileXORHash = onedriveJSONItem["file"]["hashes"]["quickXorHash"].str;
}
} else {
// Fallback: Check for SHA256Hash
if (hasSHA256Hash(onedriveJSONItem)) {
// Use the provided sha256Hash as reported by OneDrive
if (onedriveJSONItem["file"]["hashes"]["sha256Hash"].str != "") {
OneDriveFileSHA256Hash = onedriveJSONItem["file"]["hashes"]["sha256Hash"].str;
}
}
}
} else {
// file hash data missing
log.vdebug("WARNING: onedriveJSONItem['file']['hashes'] is missing - unable to compare file hash after download");
}
// Is there enough free space locally to download the file
// - We can use '.' here as we change the current working directory to the configured 'sync_dir'
ulong localActualFreeSpace = to!ulong(getAvailableDiskSpace("."));
// So that we are not responsible in making the disk 100% full if we can download the file, compare the current available space against the reservation set and file size
// The reservation value is user configurable in the config file, 50MB by default
ulong freeSpaceReservation = appConfig.getValueLong("space_reservation");
// debug output
log.vdebug("Local Disk Space Actual: ", localActualFreeSpace);
log.vdebug("Free Space Reservation: ", freeSpaceReservation);
log.vdebug("File Size to Download: ", jsonFileSize);
// Calculate if we can actually download file - is there enough free space?
if ((localActualFreeSpace < freeSpaceReservation) || (jsonFileSize > localActualFreeSpace)) {
// localActualFreeSpace is less than freeSpaceReservation .. insufficient free space
// jsonFileSize is greater than localActualFreeSpace .. insufficient free space
log.log("Downloading file ", newItemPath, " ... failed!");
log.log("Insufficient local disk space to download file");
downloadFailed = true;
} else {
// If we are in a --dry-run situation - if not, actually perform the download
if (!dryRun) {
// Attempt to download the file as there is enough free space locally
OneDriveApi downloadFileOneDriveApiInstance;
downloadFileOneDriveApiInstance = new OneDriveApi(appConfig);
try {
downloadFileOneDriveApiInstance.initialise();
downloadFileOneDriveApiInstance.downloadById(downloadDriveId, downloadItemId, newItemPath, jsonFileSize);
downloadFileOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(downloadFileOneDriveApiInstance);
} catch (OneDriveException exception) {
log.vdebug("downloadFileOneDriveApiInstance.downloadById(downloadDriveId, downloadItemId, newItemPath, jsonFileSize); generated a OneDriveException");
string thisFunctionName = getFunctionName!({});
// 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(downloadFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to download an item from OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
downloadFileItem(onedriveJSONItem);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
}
} catch (FileException e) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
downloadFailed = true;
} catch (ErrnoException e) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
downloadFailed = true;
}
// If we get to this point, something was downloaded .. does it match what we expected?
if (exists(newItemPath)) {
// When downloading some files from SharePoint, the OneDrive API reports one file size,
// but the SharePoint HTTP Server sends a totally different byte count for the same file
// we have implemented --disable-download-validation to disable these checks
if (!disableDownloadValidation) {
// A 'file' was downloaded - does what we downloaded = reported jsonFileSize or if there is some sort of funky local disk compression going on
// Does the file hash OneDrive reports match what we have locally?
string onlineFileHash;
string downloadedFileHash;
ulong downloadFileSize = getSize(newItemPath);
if (!OneDriveFileXORHash.empty) {
onlineFileHash = OneDriveFileXORHash;
// Calculate the QuickXOHash for this file
downloadedFileHash = computeQuickXorHash(newItemPath);
} else {
onlineFileHash = OneDriveFileSHA256Hash;
// Fallback: Calculate the SHA256 Hash for this file
downloadedFileHash = computeSHA256Hash(newItemPath);
}
if ((downloadFileSize == jsonFileSize) && (downloadedFileHash == onlineFileHash)) {
// Downloaded file matches size and hash
log.vdebug("Downloaded file matches reported size and reported file hash");
try {
// get the mtime from the JSON data
SysTime itemModifiedTime;
if (isItemRemote(onedriveJSONItem)) {
// remote file item
itemModifiedTime = SysTime.fromISOExtString(onedriveJSONItem["remoteItem"]["fileSystemInfo"]["lastModifiedDateTime"].str);
} else {
// not a remote item
itemModifiedTime = SysTime.fromISOExtString(onedriveJSONItem["fileSystemInfo"]["lastModifiedDateTime"].str);
}
// set the correct time on the downloaded file
log.vdebug("Calling setTimes() for this file: ", newItemPath);
setTimes(newItemPath, itemModifiedTime, itemModifiedTime);
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
} else {
// Downloaded file does not match size or hash .. which is it?
bool downloadValueMismatch = false;
// Size error?
if (downloadFileSize != jsonFileSize) {
// downloaded file size does not match
downloadValueMismatch = true;
log.vdebug("Actual file size on disk: ", downloadFileSize);
log.vdebug("OneDrive API reported size: ", jsonFileSize);
log.error("ERROR: File download size mis-match. Increase logging verbosity to determine why.");
}
// Hash Error
if (downloadedFileHash != onlineFileHash) {
// downloaded file hash does not match
downloadValueMismatch = true;
log.vdebug("Actual local file hash: ", downloadedFileHash);
log.vdebug("OneDrive API reported hash: ", onlineFileHash);
log.error("ERROR: File download hash mis-match. Increase logging verbosity to determine why.");
}
// .heic data loss check
// - https://github.com/abraunegg/onedrive/issues/2471
// - https://github.com/OneDrive/onedrive-api-docs/issues/1532
// - https://github.com/OneDrive/onedrive-api-docs/issues/1723
if (downloadValueMismatch && (toLower(extension(newItemPath)) == ".heic")) {
// Need to display a message to the user that they have experienced data loss
log.error("DATA-LOSS: File downloaded has experienced data loss due to a Microsoft OneDrive API bug. DO NOT DELETE THIS FILE ONLINE.");
log.vlog(" Please read https://github.com/OneDrive/onedrive-api-docs/issues/1723 for more details.");
}
// Add some workaround messaging for SharePoint
if (appConfig.accountType == "documentLibrary"){
// It has been seen where SharePoint / OneDrive API reports one size via the JSON
// but the content length and file size written to disk is totally different - example:
// From JSON: "size": 17133
// From HTTPS Server: < Content-Length: 19340
// with no logical reason for the difference, except for a 302 redirect before file download
log.error("INFO: It is most likely that a SharePoint OneDrive API issue is the root cause. Add --disable-download-validation to work around this issue but downloaded data integrity cannot be guaranteed.");
} else {
// other account types
log.error("INFO: Potentially add --disable-download-validation to work around this issue but downloaded data integrity cannot be guaranteed.");
}
// We do not want this local file to remain on the local file system as it failed the integrity checks
log.log("Removing file ", newItemPath, " due to failed integrity checks");
if (!dryRun) {
safeRemove(newItemPath);
}
downloadFailed = true;
}
} else {
// Download validation checks were disabled
log.vdebug("Downloaded file validation disabled due to --disable-download-validation");
log.vlog("WARNING: Skipping download integrity check for: ", newItemPath);
} // end of (!disableDownloadValidation)
} else {
log.error("ERROR: File failed to download. Increase logging verbosity to determine why.");
downloadFailed = true;
}
}
}
// File should have been downloaded
if (!downloadFailed) {
// Download did not fail
log.log("Downloading file ", newItemPath, " ... done");
// Save this item into the database
saveItem(onedriveJSONItem);
/**
log.vdebug("Inserting new item details to local database");
// What was the item that was saved
log.vdebug("item details: ", newDatabaseItem);
itemDB.upsert(newDatabaseItem);
**/
// If we are in a --dry-run situation - if we are, we need to track that we faked the download
if (dryRun) {
// track that we 'faked it'
idsFaked ~= [downloadDriveId, downloadItemId];
}
} else {
// Output download failed
log.log("Downloading file ", newItemPath, " ... failed!");
// Add the path to a list of items that failed to download
fileDownloadFailures ~= newItemPath;
}
}
}
// Test if the given item is in-sync. Returns true if the given item corresponds to the local one
bool isItemSynced(Item item, string path, string itemSource) {
if (!exists(path)) return false;
final switch (item.type) {
case ItemType.file:
if (isFile(path)) {
// can we actually read the local file?
if (readLocalFile(path)){
// local file is readable
SysTime localModifiedTime = timeLastModified(path).toUTC();
SysTime itemModifiedTime = item.mtime;
// Reduce time resolution to seconds before comparing
localModifiedTime.fracSecs = Duration.zero;
itemModifiedTime.fracSecs = Duration.zero;
if (localModifiedTime == itemModifiedTime) {
return true;
} else {
log.vlog("Local item time discrepancy detected: ", path);
log.vlog("This local item has a different modified time ", localModifiedTime, " when compared to ", itemSource, " modified time ", itemModifiedTime);
// The file has been modified ... is the hash the same?
// Test the file hash as the date / time stamp is different
// Generating a hash is computationally expensive - we only generate the hash if timestamp was different
if (testFileHash(path, item)) {
// The hash is the same .. so we need to fix-up the timestamp depending on where it is wrong
log.vlog("Local item has the same hash value as the item online - correcting timestamp");
// Test if the local timestamp is newer
if (localModifiedTime > itemModifiedTime) {
// The source of the out-of-date timestamp was OneDrive and this needs to be corrected to avoid always generating a hash test if timestamp is different
log.vlog("The source of the incorrect timestamp was OneDrive online - correcting timestamp online");
if (!dryRun) {
// Attempt to update the online date time stamp
uploadLastModifiedTime(item.driveId, item.id, localModifiedTime.toUTC(), item.eTag);
}
} else {
// The source of the out-of-date timestamp was the local file and this needs to be corrected to avoid always generating a hash test if timestamp is different
log.vlog("The source of the incorrect timestamp was the local file - correcting timestamp locally");
if (!dryRun) {
log.vdebug("Calling setTimes() for this file: ", path);
setTimes(path, item.mtime, item.mtime);
}
}
return true;
} else {
// The hash is different so the content of the file has to be different as to what is stored online
log.vlog("The local item has a different hash when compared to ", itemSource, " item hash");
return false;
}
}
} else {
// Unable to read local file
log.log("Unable to determine the sync state of this file as it cannot be read (file permissions or file corruption): ", path);
return false;
}
} else {
log.vlog("The local item is a directory but should be a file");
}
break;
case ItemType.dir:
case ItemType.remote:
if (isDir(path)) {
return true;
} else {
log.vlog("The local item is a file but should be a directory");
}
break;
case ItemType.unknown:
// Unknown type - return true but we dont action or sync these items
return true;
}
return false;
}
// Get the /delta data using the provided details
JSONValue getDeltaChangesByItemId(string selectedDriveId, string selectedItemId, string providedDeltaLink, OneDriveApi getDeltaQueryOneDriveApiInstance) {
// Function variables
JSONValue deltaChangesBundle;
// Get the /delta data for this account | driveId | deltaLink combination
log.vdebug("------------------------------------------------------------------");
log.vdebug("selectedDriveId: ", selectedDriveId);
log.vdebug("selectedItemId: ", selectedItemId);
log.vdebug("providedDeltaLink: ", providedDeltaLink);
log.vdebug("------------------------------------------------------------------");
try {
deltaChangesBundle = getDeltaQueryOneDriveApiInstance.viewChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink);
} catch (OneDriveException exception) {
// caught an exception
log.vdebug("getDeltaQueryOneDriveApiInstance.viewChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink) generated a OneDriveException");
auto errorArray = splitLines(exception.msg);
string thisFunctionName = getFunctionName!({});
// 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(getDeltaQueryOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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
log.log(errorArray[0], " when attempting to query OneDrive API for Delta Changes - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// dont retry request, loop back to calling function
log.vdebug("Looping back after failure");
deltaChangesBundle = null;
} else {
// Default operation if not 408,429,503,504 errors
if (exception.httpStatusCode == 410) {
log.log("\nWARNING: The OneDrive API responded with an error that indicates the locally stored deltaLink value is invalid");
// Essentially the 'providedDeltaLink' that we have stored is no longer available ... re-try without the stored deltaLink
log.log("WARNING: Retrying OneDrive API call without using the locally stored deltaLink value");
// Configure an empty deltaLink
log.vdebug("Delta link expired for 'getDeltaQueryOneDriveApiInstance.viewChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink)', setting 'deltaLink = null'");
string emptyDeltaLink = "";
// retry with empty deltaLink
deltaChangesBundle = getDeltaQueryOneDriveApiInstance.viewChangesByItemId(selectedDriveId, selectedItemId, emptyDeltaLink);
} else {
// display what the error is
log.log("CODING TO DO: Hitting this failure error output");
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
deltaChangesBundle = null;
}
}
}
return deltaChangesBundle;
}
// Common code to handle a 408 or 429 response from the OneDrive API
void handleOneDriveThrottleRequest(OneDriveApi activeOneDriveApiInstance) {
// If OneDrive sends a status code 429 then this function will be used to process the Retry-After response header which contains the value by which we need to wait
log.vdebug("Handling a OneDrive HTTP 429 Response Code (Too Many Requests)");
// Read in the Retry-After HTTP header as set and delay as per this value before retrying the request
auto retryAfterValue = activeOneDriveApiInstance.getRetryAfterValue();
log.vdebug("Using Retry-After Value = ", retryAfterValue);
// HTTP request returned status code 429 (Too Many Requests)
// https://github.com/abraunegg/onedrive/issues/133
// https://github.com/abraunegg/onedrive/issues/815
ulong delayBeforeRetry = 0;
if (retryAfterValue != 0) {
// Use the HTTP Response Header Value
delayBeforeRetry = retryAfterValue;
} else {
// Use a 120 second delay as a default given header value was zero
// This value is based on log files and data when determining correct process for 429 response handling
delayBeforeRetry = 120;
// Update that we are over-riding the provided value with a default
log.vdebug("HTTP Response Header retry-after value was 0 - Using a preconfigured default of: ", delayBeforeRetry);
}
// Sleep thread as per request
log.log("Thread sleeping due to 'HTTP request returned status code 429' - The request has been throttled");
log.log("Sleeping for ", delayBeforeRetry, " seconds");
Thread.sleep(dur!"seconds"(delayBeforeRetry));
// Reset retry-after value to zero as we have used this value now and it may be changed in the future to a different value
activeOneDriveApiInstance.resetRetryAfterValue();
}
// If the JSON response is not correct JSON object, exit
void invalidJSONResponseFromOneDriveAPI() {
log.error("ERROR: Query of the OneDrive API returned an invalid JSON response");
// Must exit
exit(-1);
}
// Handle an unhandled API error
void defaultUnhandledHTTPErrorCode(OneDriveException exception) {
// display error
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
// Must exit here
exit(-1);
}
// Display the pertinant details of the sync engine
void displaySyncEngineDetails() {
// Display accountType, defaultDriveId, defaultRootId & remainingFreeSpace for verbose logging purposes
//log.vlog("Application version: ", strip(import("version")));
string tempVersion = "v2.5.0-alpha-3" ~ " GitHub version: " ~ strip(import("version"));
log.vlog("Application version: ", tempVersion);
log.vlog("Account Type: ", appConfig.accountType);
log.vlog("Default Drive ID: ", appConfig.defaultDriveId);
log.vlog("Default Root ID: ", appConfig.defaultRootId);
// What do we display here for space remaining
if (appConfig.remainingFreeSpace > 0) {
// Display the actual value
log.vlog("Remaining Free Space: ", (appConfig.remainingFreeSpace/1024) , " KB");
} else {
// zero or non-zero value or restricted
if (!appConfig.quotaRestricted){
log.vlog("Remaining Free Space: 0 KB");
} else {
log.vlog("Remaining Free Space: Not Available");
}
}
}
// Query itemdb.computePath() and catch potential assert when DB consistency issue occurs
string computeItemPath(string thisDriveId, string thisItemId) {
// static declare this for this function
static import core.exception;
string calculatedPath;
log.vdebug("Attempting to calculate local filesystem path for ", thisDriveId, " and ", thisItemId);
try {
calculatedPath = itemDB.computePath(thisDriveId, thisItemId);
} catch (core.exception.AssertError) {
// broken tree in the database, we cant compute the path for this item id, exit
log.error("ERROR: A database consistency issue has been caught. A --resync is needed to rebuild the database.");
// Must exit here to preserve data
exit(-1);
}
// return calculated path as string
return calculatedPath;
}
// Try and compute the file hash for the given item
bool testFileHash(string path, Item item) {
// Generate QuickXORHash first before attempting to generate any other type of hash
if (item.quickXorHash) {
if (item.quickXorHash == computeQuickXorHash(path)) return true;
} else if (item.sha256Hash) {
if (item.sha256Hash == computeSHA256Hash(path)) return true;
}
return false;
}
// Process items that need to be removed
void processDeleteItems() {
foreach_reverse (i; idsToDelete) {
Item item;
string path;
if (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db
// Compute this item path
path = computeItemPath(i[0], i[1]);
// Log the action if the path exists .. it may of already been removed and this is a legacy array item
if (exists(path)) {
if (item.type == ItemType.file) {
log.log("Trying to delete file ", path);
} else {
log.log("Trying to delete directory ", path);
}
}
// Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy
itemDB.deleteById(item.driveId, item.id);
if (item.remoteDriveId != null) {
// delete the linked remote folder
itemDB.deleteById(item.remoteDriveId, item.remoteId);
}
// Add to pathFakeDeletedArray
// We dont want to try and upload this item again, so we need to track this object
if (dryRun) {
// We need to add './' here so that it can be correctly searched to ensure it is not uploaded
string pathToAdd = "./" ~ path;
pathFakeDeletedArray ~= pathToAdd;
}
bool needsRemoval = false;
if (exists(path)) {
// path exists on the local system
// make sure that the path refers to the correct item
Item pathItem;
if (itemDB.selectByPath(path, item.driveId, pathItem)) {
if (pathItem.id == item.id) {
needsRemoval = true;
} else {
log.log("Skipped due to id difference!");
}
} else {
// item has disappeared completely
needsRemoval = true;
}
}
if (needsRemoval) {
// Log the action
if (item.type == ItemType.file) {
log.log("Deleting file ", path);
} else {
log.log("Deleting directory ", path);
}
// Perform the action
if (!dryRun) {
if (isFile(path)) {
remove(path);
} else {
try {
// Remove any children of this path if they still exist
// Resolve 'Directory not empty' error when deleting local files
foreach (DirEntry child; dirEntries(path, SpanMode.depth, false)) {
attrIsDir(child.linkAttributes) ? rmdir(child.name) : remove(child.name);
}
// Remove the path now that it is empty of children
rmdirRecurse(path);
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
}
}
}
}
if (!dryRun) {
// Cleanup array memory
idsToDelete = [];
}
}
// Update the timestamp of an object online
void uploadLastModifiedTime(string driveId, string id, SysTime mtime, string eTag) {
string itemModifiedTime;
itemModifiedTime = mtime.toISOExtString();
JSONValue data = [
"fileSystemInfo": JSONValue([
"lastModifiedDateTime": itemModifiedTime
])
];
// What eTag value do we use?
string eTagValue;
if (appConfig.accountType == "personal") {
eTagValue = null;
} else {
eTagValue = eTag;
}
JSONValue response;
// Create a new OneDrive API instance
OneDriveApi uploadLastModifiedTimeApiInstance;
uploadLastModifiedTimeApiInstance = new OneDriveApi(appConfig);
uploadLastModifiedTimeApiInstance.initialise();
// Try and update the online last modified time
try {
// Use this instance
response = uploadLastModifiedTimeApiInstance.updateById(driveId, id, data, eTagValue);
// Shut the instance down
uploadLastModifiedTimeApiInstance.shutdown();
// Free object and memory
object.destroy(uploadLastModifiedTimeApiInstance);
// Is the response a valid JSON object - validation checking done in saveItem
saveItem(response);
} catch (OneDriveException exception) {
string thisFunctionName = getFunctionName!({});
// 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(uploadLastModifiedTimeApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to update the timestamp on an item on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
uploadLastModifiedTime(driveId, id, mtime, eTag);
return;
} else {
// Default operation if not 408,429,503,504 errors
if (exception.httpStatusCode == 409) {
// ETag does not match current item's value - use a null eTag
log.vdebug("Retrying Function: ", thisFunctionName);
uploadLastModifiedTime(driveId, id, mtime, null);
} else {
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
}
}
}
}
// Perform a database integrity check - checking all the items that are in-sync at the moment, validating what we know should be on disk, to what is actually on disk
void performDatabaseConsistencyAndIntegrityCheck() {
// Log what we are doing
if (!appConfig.surpressLoggingOutput) {
log.log("Performing a database consistency and integrity check on locally stored data ... ");
}
// What driveIDsArray do we use? If we are doing a --single-directory we need to use just the drive id associated with that operation
string[] consistencyCheckDriveIdsArray;
if (singleDirectoryScope) {
consistencyCheckDriveIdsArray ~= singleDirectoryScopeDriveId;
} else {
consistencyCheckDriveIdsArray = driveIDsArray;
}
// Create a new DB blank item
Item item;
// Use the array we populate, rather than selecting all distinct driveId's from the database
foreach (driveId; consistencyCheckDriveIdsArray) {
// Make the logging more accurate - we cant update driveId as this then breaks the below queries
log.vlog("Processing DB entries for this Drive ID: ", driveId);
// 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 did, we self generated a /delta response, thus need to now process elements that are still flagged as out-of-sync
if ((singleDirectoryScope) || (nationalCloudDeployment) || (cleanupLocalFiles)) {
// Any entry in the DB than is flagged as out-of-sync needs to be cleaned up locally first before we scan the entire DB
// Normally, this is done at the end of processing all /delta queries, however when using --single-directory or a National Cloud Deployments is configured
// We cant use /delta to query the OneDrive API as National Cloud Deployments dont support /delta
// https://docs.microsoft.com/en-us/graph/deployments#supported-features
// We dont use /delta for --single-directory as, in order to sync a single path with /delta, we need to query the entire OneDrive API JSON data to then filter out
// objects that we dont want, thus, it is easier to use the same method as National Cloud Deployments, but query just the objects we are after
// For each unique OneDrive driveID we know about
Item[] outOfSyncItems = itemDB.selectOutOfSyncItems(driveId);
foreach (outOfSyncItem; outOfSyncItems) {
if (!dryRun) {
// clean up idsToDelete
idsToDelete.length = 0;
assumeSafeAppend(idsToDelete);
// flag to delete local file as it now is no longer in sync with OneDrive
log.vdebug("Flagging to delete local item as it now is no longer in sync with OneDrive");
log.vdebug("outOfSyncItem: ", outOfSyncItem);
idsToDelete ~= [outOfSyncItem.driveId, outOfSyncItem.id];
// delete items in idsToDelete
if (idsToDelete.length > 0) processDeleteItems();
}
}
// Fetch database items associated with this path
Item[] driveItems;
if (singleDirectoryScope) {
// Use the --single-directory items we previously configured
// - query database for children objects using those items
driveItems = getChildren(singleDirectoryScopeDriveId, singleDirectoryScopeItemId);
} else {
// Check everything associated with each driveId we know about
log.vdebug("Selecting DB items via itemDB.selectByDriveId(driveId)");
// Query database
driveItems = itemDB.selectByDriveId(driveId);
}
log.vdebug("Database items to process for this driveId: ", driveItems.count);
// Process each database database item associated with the driveId
foreach(dbItem; driveItems) {
// Does it still exist on disk in the location the DB thinks it is
checkDatabaseItemForConsistency(dbItem);
}
} else {
// Check everything associated with each driveId we know about
log.vdebug("Selecting DB items via itemDB.selectByDriveId(driveId)");
// Query database
auto driveItems = itemDB.selectByDriveId(driveId);
log.vdebug("Database items to process for this driveId: ", driveItems.count);
// Process each database database item associated with the driveId
foreach(dbItem; driveItems) {
// Does it still exist on disk in the location the DB thinks it is
checkDatabaseItemForConsistency(dbItem);
}
}
}
// Are we doing a --download-only sync?
if (!appConfig.getValueBool("download_only")) {
// Do we have any known items, where the content has changed locally, that needs to be uploaded?
if (!databaseItemsWhereContentHasChanged.empty) {
// There are changed local files that were in the DB to upload
log.log("Changed local items to upload to OneDrive: ", databaseItemsWhereContentHasChanged.length);
processChangedLocalItemsToUpload();
// Cleanup array memory
databaseItemsWhereContentHasChanged = [];
}
}
}
// Check this Database Item for its consistency on disk
void checkDatabaseItemForConsistency(Item dbItem) {
// What is the local path item
string localFilePath;
// Do we want to onward process this item?
bool unwanted = false;
// Compute this dbItem path early as we we use this path often
localFilePath = buildNormalizedPath(computeItemPath(dbItem.driveId, dbItem.id));
// To improve logging output for this function, what is the 'logical path'?
string logOutputPath;
if (localFilePath == ".") {
// get the configured sync_dir
logOutputPath = buildNormalizedPath(appConfig.getValueString("sync_dir"));
} else {
// use what was computed
logOutputPath = localFilePath;
}
// Log what we are doing
log.vlog("Processing ", logOutputPath);
// Determine which action to take
final switch (dbItem.type) {
case ItemType.file:
// Logging output
checkFileDatabaseItemForConsistency(dbItem, localFilePath);
break;
case ItemType.dir:
// Logging output
checkDirectoryDatabaseItemForConsistency(dbItem, localFilePath);
break;
case ItemType.remote:
// checkRemoteDirectoryDatabaseItemForConsistency(dbItem, localFilePath);
break;
case ItemType.unknown:
// Unknown type - we dont action these items
break;
}
}
// Perform the database consistency check on this file item
void checkFileDatabaseItemForConsistency(Item dbItem, string localFilePath) {
// What is the source of this item data?
string itemSource = "database";
// Does this item|file still exist on disk?
if (exists(localFilePath)) {
// Path exists locally, is this path a file?
if (isFile(localFilePath)) {
// Can we actually read the local file?
if (readLocalFile(localFilePath)){
// File is readable
SysTime localModifiedTime = timeLastModified(localFilePath).toUTC();
SysTime itemModifiedTime = dbItem.mtime;
// Reduce time resolution to seconds before comparing
itemModifiedTime.fracSecs = Duration.zero;
localModifiedTime.fracSecs = Duration.zero;
if (localModifiedTime != itemModifiedTime) {
// The modified dates are different
log.vdebug("The local item has a different modified time ", localModifiedTime, " when compared to ", itemSource, " modified time ", itemModifiedTime);
// Test the file hash
if (!testFileHash(localFilePath, dbItem)) {
// Is the local file 'newer' or 'older' (ie was an old file 'restored locally' by a different backup / replacement process?)
if (localModifiedTime >= itemModifiedTime) {
// Local file is newer
if (!appConfig.getValueBool("download_only")) {
log.vlog("The file content has changed locally and has a newer timestamp, thus needs to be uploaded to OneDrive");
// Add to an array of files we need to upload as this file has changed locally in-between doing the /delta check and performing this check
databaseItemsWhereContentHasChanged ~= [dbItem.driveId, dbItem.id, localFilePath];
} else {
log.vlog("The file content has changed locally and has a newer timestamp. The file will remain different to online file due to --download-only being used");
}
} else {
// Local file is older - data recovery process? something else?
if (!appConfig.getValueBool("download_only")) {
log.vlog("The file content has changed locally and file now has a older timestamp. Uploading this file to OneDrive may potentially cause data-loss online");
// Add to an array of files we need to upload as this file has changed locally in-between doing the /delta check and performing this check
databaseItemsWhereContentHasChanged ~= [dbItem.driveId, dbItem.id, localFilePath];
} else {
log.vlog("The file content has changed locally and file now has a older timestamp. The file will remain different to online file due to --download-only being used");
}
}
} else {
// The file contents have not changed, but the modified timestamp has
log.vlog("The last modified timestamp has changed however the file content has not changed");
log.vlog("The local item has the same hash value as the item online - correcting timestamp online");
if (!dryRun) {
// Attempt to update the online date time stamp
uploadLastModifiedTime(dbItem.driveId, dbItem.id, localModifiedTime.toUTC(), dbItem.eTag);
}
}
} else {
// The file has not changed
log.vlog("The file has not changed");
}
} else {
//The file is not readable - skipped
log.log("Skipping processing this file as it cannot be read (file permissions or file corruption): ", localFilePath);
}
} else {
// The item was a file but now is a directory
log.vlog("The item was a file but now is a directory");
}
} else {
// File does not exist locally, but is in our database as a dbItem containing all the data was passed into this function
// If we are in a --dry-run situation - this file may never have existed as we never downloaded it
if (!dryRun) {
// Not --dry-run situation
log.vlog("The file has been deleted locally");
// Upload to OneDrive the instruction to delete this item. This will handle the 'noRemoteDelete' flag if set
uploadDeletedItem(dbItem, localFilePath);
} else {
// We are in a --dry-run situation, file appears to have been deleted locally - this file may never have existed locally as we never downloaded it due to --dry-run
// Did we 'fake create it' as part of --dry-run ?
bool idsFakedMatch = false;
foreach (i; idsFaked) {
if (i[1] == dbItem.id) {
log.vdebug("Matched faked file which is 'supposed' to exist but not created due to --dry-run use");
log.vlog("The file has not changed");
idsFakedMatch = true;
}
}
if (!idsFakedMatch) {
// dbItem.id did not match a 'faked' download new file creation - so this in-sync object was actually deleted locally, but we are in a --dry-run situation
log.vlog("The file has been deleted locally");
// Upload to OneDrive the instruction to delete this item. This will handle the 'noRemoteDelete' flag if set
uploadDeletedItem(dbItem, localFilePath);
}
}
}
}
// Perform the database consistency check on this directory item
void checkDirectoryDatabaseItemForConsistency(Item dbItem, string localFilePath) {
// What is the source of this item data?
string itemSource = "database";
// Does this item|directory still exist on disk?
if (exists(localFilePath)) {
// Fix https://github.com/abraunegg/onedrive/issues/1915
try {
if (!isDir(localFilePath)) {
log.vlog("The item was a directory but now it is a file");
uploadDeletedItem(dbItem, localFilePath);
uploadNewFile(localFilePath);
} else {
// Directory still exists locally
log.vlog("The directory has not changed");
// When we are using --single-directory, we use a the getChildren() call to get all children of a path, meaning all children are already traversed
// Thus, if we traverse the path of this directory .. we end up with double processing & log output .. which is not ideal
if (!singleDirectoryScope) {
// loop through the children
foreach (Item child; itemDB.selectChildren(dbItem.driveId, dbItem.id)) {
checkDatabaseItemForConsistency(child);
}
}
}
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
} else {
// Directory does not exist locally, but it is in our database as a dbItem containing all the data was passed into this function
// If we are in a --dry-run situation - this directory may never have existed as we never created it
if (!dryRun) {
// Not --dry-run situation
if (!appConfig.getValueBool("monitor")) {
// Not in --monitor mode
log.vlog("The directory has been deleted locally");
} else {
// Appropriate message as we are in --monitor mode
log.vlog("The directory appears to have been deleted locally .. but we are running in --monitor mode. This may have been 'moved' on the local filesystem rather than being 'deleted'");
log.vdebug("Most likely cause - 'inotify' event was missing for whatever action was taken locally or action taken when application was stopped");
}
// A moved directory will be uploaded as 'new', delete the old directory and database reference
// Upload to OneDrive the instruction to delete this item. This will handle the 'noRemoteDelete' flag if set
uploadDeletedItem(dbItem, localFilePath);
} else {
// We are in a --dry-run situation, directory appears to have been deleted locally - this directory may never have existed locally as we never created it due to --dry-run
// Did we 'fake create it' as part of --dry-run ?
bool idsFakedMatch = false;
foreach (i; idsFaked) {
if (i[1] == dbItem.id) {
log.vdebug("Matched faked dir which is 'supposed' to exist but not created due to --dry-run use");
log.vlog("The directory has not changed");
idsFakedMatch = true;
}
}
if (!idsFakedMatch) {
// dbItem.id did not match a 'faked' download new directory creation - so this in-sync object was actually deleted locally, but we are in a --dry-run situation
log.vlog("The directory has been deleted locally");
// Upload to OneDrive the instruction to delete this item. This will handle the 'noRemoteDelete' flag if set
uploadDeletedItem(dbItem, localFilePath);
} else {
// When we are using --single-directory, we use a the getChildren() call to get all children of a path, meaning all children are already traversed
// Thus, if we traverse the path of this directory .. we end up with double processing & log output .. which is not ideal
if (!singleDirectoryScope) {
// loop through the children
foreach (Item child; itemDB.selectChildren(dbItem.driveId, dbItem.id)) {
checkDatabaseItemForConsistency(child);
}
}
}
}
}
}
// Does this local path (directory or file) conform with the Microsoft Naming Restrictions?
bool checkPathAgainstMicrosoftNamingRestrictions(string localFilePath) {
// Check if the given path violates certain Microsoft restrictions and limitations
// Return a true|false response
bool invalidPath = false;
// Check against Microsoft OneDrive restriction and limitations about Windows naming files
if (!invalidPath) {
if (!isValidName(localFilePath)) {
log.logAndNotify("Skipping item - invalid name (Microsoft Naming Convention): ", localFilePath);
invalidPath = true;
}
}
// Check for bad whitespace items
if (!invalidPath) {
if (!containsBadWhiteSpace(localFilePath)) {
log.logAndNotify("Skipping item - invalid name (Contains an invalid whitespace item): ", localFilePath);
invalidPath = true;
}
}
// Check for HTML ASCII Codes as part of file name
if (!invalidPath) {
if (!containsASCIIHTMLCodes(localFilePath)) {
log.logAndNotify("Skipping item - invalid name (Contains HTML ASCII Code): ", localFilePath);
invalidPath = true;
}
}
// Return if this is a valid path
return invalidPath;
}
// Does this local path (directory or file) get excluded from any operation based on any client side filtering rules?
bool checkPathAgainstClientSideFiltering(string localFilePath) {
// Check the path against client side filtering rules
// - check_nosync
// - skip_dotfiles
// - skip_symlinks
// - skip_file
// - skip_dir
// - sync_list
// - skip_size
// Return a true|false response
bool clientSideRuleExcludesPath = false;
// does the path exist?
if (!exists(localFilePath)) {
// path does not exist - we cant review any client side rules on something that does not exist locally
return clientSideRuleExcludesPath;
}
// - check_nosync
if (!clientSideRuleExcludesPath) {
// Do we need to check for .nosync? Only if --check-for-nosync was passed in
if (appConfig.getValueBool("check_nosync")) {
if (exists(localFilePath ~ "/.nosync")) {
log.vlog("Skipping item - .nosync found & --check-for-nosync enabled: ", localFilePath);
clientSideRuleExcludesPath = true;
}
}
}
// - skip_dotfiles
if (!clientSideRuleExcludesPath) {
// Do we need to check skip dot files if configured
if (appConfig.getValueBool("skip_dotfiles")) {
if (isDotFile(localFilePath)) {
log.vlog("Skipping item - .file or .folder: ", localFilePath);
clientSideRuleExcludesPath = true;
}
}
}
// - skip_symlinks
if (!clientSideRuleExcludesPath) {
// Is the path a symbolic link
if (isSymlink(localFilePath)) {
// if config says so we skip all symlinked items
if (appConfig.getValueBool("skip_symlinks")) {
log.vlog("Skipping item - skip symbolic links configured: ", localFilePath);
clientSideRuleExcludesPath = true;
}
// skip unexisting symbolic links
else if (!exists(readLink(localFilePath))) {
// reading the symbolic link failed - is the link a relative symbolic link
// drwxrwxr-x. 2 alex alex 46 May 30 09:16 .
// drwxrwxr-x. 3 alex alex 35 May 30 09:14 ..
// lrwxrwxrwx. 1 alex alex 61 May 30 09:16 absolute.txt -> /home/alex/OneDrivePersonal/link_tests/intercambio/prueba.txt
// lrwxrwxrwx. 1 alex alex 13 May 30 09:16 relative.txt -> ../prueba.txt
//
// absolute links will be able to be read, but 'relative' links will fail, because they cannot be read based on the current working directory 'sync_dir'
string currentSyncDir = getcwd();
string fullLinkPath = buildNormalizedPath(absolutePath(localFilePath));
string fileName = baseName(fullLinkPath);
string parentLinkPath = dirName(fullLinkPath);
// test if this is a 'relative' symbolic link
chdir(parentLinkPath);
auto relativeLink = readLink(fileName);
auto relativeLinkTest = exists(readLink(fileName));
// reset back to our 'sync_dir'
chdir(currentSyncDir);
// results
if (relativeLinkTest) {
log.vdebug("Not skipping item - symbolic link is a 'relative link' to target ('", relativeLink, "') which can be supported: ", localFilePath);
} else {
log.logAndNotify("Skipping item - invalid symbolic link: ", localFilePath);
clientSideRuleExcludesPath = true;
}
}
}
}
// Is this item excluded by user configuration of skip_dir or skip_file?
if (!clientSideRuleExcludesPath) {
if (localFilePath != ".") {
// skip_dir handling
if (isDir(localFilePath)) {
log.vdebug("Checking local path: ", localFilePath);
// Only check path if config is != ""
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(localFilePath.strip('.'))) {
log.vlog("Skipping item - excluded by skip_dir config: ", localFilePath);
clientSideRuleExcludesPath = true;
}
}
}
// skip_file handling
if (isFile(localFilePath)) {
log.vdebug("Checking file: ", localFilePath);
// The path that needs to be checked needs to include the '/'
// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched
if (selectiveSync.isFileNameExcluded(localFilePath.strip('.'))) {
log.vlog("Skipping item - excluded by skip_file config: ", localFilePath);
clientSideRuleExcludesPath = true;
}
}
}
}
// Is this item excluded by user configuration of sync_list?
if (!clientSideRuleExcludesPath) {
if (localFilePath != ".") {
if (syncListConfigured) {
// sync_list configured and in use
if (selectiveSync.isPathExcludedViaSyncList(localFilePath)) {
if ((isFile(localFilePath)) && (appConfig.getValueBool("sync_root_files")) && (rootName(localFilePath.strip('.').strip('/')) == "")) {
log.vdebug("Not skipping path due to sync_root_files inclusion: ", localFilePath);
} else {
if (exists(appConfig.syncListFilePath)){
// skipped most likely due to inclusion in sync_list
log.vlog("Skipping item - excluded by sync_list config: ", localFilePath);
clientSideRuleExcludesPath = true;
} else {
// skipped for some other reason
log.vlog("Skipping item - path excluded by user config: ", localFilePath);
clientSideRuleExcludesPath = true;
}
}
}
}
}
}
// Check if this is excluded by a user set maximum filesize to upload
if (!clientSideRuleExcludesPath) {
if (isFile(localFilePath)) {
if (fileSizeLimit != 0) {
// Get the file size
ulong thisFileSize = getSize(localFilePath);
if (thisFileSize >= fileSizeLimit) {
log.vlog("Skipping item - excluded by skip_size config: ", localFilePath, " (", thisFileSize/2^^20," MB)");
}
}
}
}
return clientSideRuleExcludesPath;
}
// Does this JSON item (as received from OneDrive API) get excluded from any operation based on any client side filtering rules?
// This function is only used when we are fetching objects from the OneDrive API using a /children query to help speed up what object we query
bool checkJSONAgainstClientSideFiltering(JSONValue onedriveJSONItem) {
bool clientSideRuleExcludesPath = false;
// Check the path against client side filtering rules
// - check_nosync (MISSING)
// - skip_dotfiles (MISSING)
// - skip_symlinks (MISSING)
// - skip_file
// - skip_dir
// - sync_list
// - skip_size (MISSING)
// Return a true|false response
// Use the JSON elements rather can computing a DB struct via makeItem()
string thisItemId = onedriveJSONItem["id"].str;
string thisItemDriveId = onedriveJSONItem["parentReference"]["driveId"].str;
string thisItemParentId = onedriveJSONItem["parentReference"]["id"].str;
string thisItemName = onedriveJSONItem["name"].str;
// Is this parent is in the database
bool parentInDatabase = false;
// Calculate if the Parent Item is in the database so that it can be re-used
parentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId);
// Check if this is excluded by config option: skip_dir
if (!clientSideRuleExcludesPath) {
// Is the item a folder?
if (isItemFolder(onedriveJSONItem)) {
// Only check path if config is != ""
if (!appConfig.getValueString("skip_dir").empty) {
// work out the 'snippet' path where this folder would be created
string simplePathToCheck = "";
string complexPathToCheck = "";
string matchDisplay = "";
if (hasParentReference(onedriveJSONItem)) {
// we need to workout the FULL path for this item
// simple path
if (("name" in onedriveJSONItem["parentReference"]) != null) {
simplePathToCheck = onedriveJSONItem["parentReference"]["name"].str ~ "/" ~ onedriveJSONItem["name"].str;
} else {
simplePathToCheck = onedriveJSONItem["name"].str;
}
log.vdebug("skip_dir path to check (simple): ", simplePathToCheck);
// complex path
if (parentInDatabase) {
// build up complexPathToCheck
//complexPathToCheck = buildNormalizedPath(newItemPath);
complexPathToCheck = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName;
} else {
log.vdebug("Parent details not in database - unable to compute complex path to check");
}
if (!complexPathToCheck.empty) {
log.vdebug("skip_dir path to check (complex): ", complexPathToCheck);
}
} else {
simplePathToCheck = onedriveJSONItem["name"].str;
}
// If 'simplePathToCheck' or 'complexPathToCheck' is of the following format: root:/folder
// then isDirNameExcluded matching will not work
// Clean up 'root:' if present
if (startsWith(simplePathToCheck, "root:")){
log.vdebug("Updating simplePathToCheck to remove 'root:'");
simplePathToCheck = strip(simplePathToCheck, "root:");
}
if (startsWith(complexPathToCheck, "root:")){
log.vdebug("Updating complexPathToCheck to remove 'root:'");
complexPathToCheck = strip(complexPathToCheck, "root:");
}
// OK .. what checks are we doing?
if ((!simplePathToCheck.empty) && (complexPathToCheck.empty)) {
// just a simple check
log.vdebug("Performing a simple check only");
clientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(simplePathToCheck);
} else {
// simple and complex
log.vdebug("Performing a simple then complex path match if required");
// simple first
log.vdebug("Performing a simple check first");
clientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(simplePathToCheck);
matchDisplay = simplePathToCheck;
if (!clientSideRuleExcludesPath) {
log.vdebug("Simple match was false, attempting complex match");
// simple didnt match, perform a complex check
clientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(complexPathToCheck);
matchDisplay = complexPathToCheck;
}
}
// result
log.vdebug("skip_dir exclude result (directory based): ", clientSideRuleExcludesPath);
if (clientSideRuleExcludesPath) {
// This path should be skipped
log.vlog("Skipping item - excluded by skip_dir config: ", matchDisplay);
}
}
}
}
// Check if this is excluded by config option: skip_file
if (!clientSideRuleExcludesPath) {
// is the item a file ?
if (isFileItem(onedriveJSONItem)) {
// JSON item is a file
// skip_file can contain 4 types of entries:
// - wildcard - *.txt
// - text + wildcard - name*.txt
// - full path + combination of any above two - /path/name*.txt
// - full path to file - /path/to/file.txt
string exclusionTestPath = "";
// is the parent id in the database?
if (parentInDatabase) {
// parent id is in the database, so we can try and calculate the full file path
string jsonItemPath = "";
// Compute this item path & need the full path for this file
jsonItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName;
// Log the calculation
log.vdebug("New Item calculated full path is: ", jsonItemPath);
// The path that needs to be checked needs to include the '/'
// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched
// However, as 'path' used throughout, use a temp variable with this modification so that we use the temp variable for exclusion checks
if (!startsWith(jsonItemPath, "/")){
// Add '/' to the path
exclusionTestPath = '/' ~ jsonItemPath;
}
// what are we checking
log.vdebug("skip_file item to check (full calculated path): ", exclusionTestPath);
} else {
// parent not in database, we can only check using this JSON item's name
if (!startsWith(thisItemName, "/")){
// Add '/' to the path
exclusionTestPath = '/' ~ thisItemName;
}
// what are we checking
log.vdebug("skip_file item to check (file name only - parent path not in database): ", exclusionTestPath);
clientSideRuleExcludesPath = selectiveSync.isFileNameExcluded(exclusionTestPath);
}
// Perform the 'skip_file' evaluation
clientSideRuleExcludesPath = selectiveSync.isFileNameExcluded(exclusionTestPath);
log.vdebug("Result: ", clientSideRuleExcludesPath);
if (clientSideRuleExcludesPath) {
// This path should be skipped
log.vlog("Skipping item - excluded by skip_file config: ", exclusionTestPath);
}
}
}
// Check if this is included or excluded by use of sync_list
if (!clientSideRuleExcludesPath) {
// No need to try and process something against a sync_list if it has been configured
if (syncListConfigured) {
// Compute the item path if empty - as to check sync_list we need an actual path to check
// What is the path of the new item
string newItemPath;
// Is the parent in the database? If not, we cannot compute the the full path based on the database entries
// In a --resync scenario - the database is empty
if (parentInDatabase) {
// Calculate this items path based on database entries
newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName;
} else {
// parent not in the database
if (("path" in onedriveJSONItem["parentReference"]) != null) {
// If there is a parent reference path, try and use it
string selfBuiltPath = onedriveJSONItem["parentReference"]["path"].str ~ "/" ~ onedriveJSONItem["name"].str;
auto splitPath = selfBuiltPath.split("root:");
newItemPath = splitPath[1];
} else {
// no parent reference path available
newItemPath = thisItemName;
}
}
// Update newItemPath
if(newItemPath[0] == '/') {
newItemPath = newItemPath[1..$];
}
// What path are we checking?
log.vdebug("sync_list item to check: ", newItemPath);
// Unfortunatly there is no avoiding this call to check if the path is excluded|included via sync_list
if (selectiveSync.isPathExcludedViaSyncList(newItemPath)) {
// selective sync advised to skip, however is this a file and are we configured to upload / download files in the root?
if ((isItemFile(onedriveJSONItem)) && (appConfig.getValueBool("sync_root_files")) && (rootName(newItemPath) == "") ) {
// This is a file
// We are configured to sync all files in the root
// This is a file in the logical root
clientSideRuleExcludesPath = false;
} else {
// path is unwanted
clientSideRuleExcludesPath = true;
log.vlog("Skipping item - excluded by sync_list config: ", newItemPath);
}
}
}
}
// return if path is excluded
return clientSideRuleExcludesPath;
}
// Process the list of local changes to upload to OneDrive
void processChangedLocalItemsToUpload() {
// Each element in this array 'databaseItemsWhereContentHasChanged' is an Database Item ID that has been modified locally
ulong batchSize = appConfig.concurrentThreads;
ulong batchCount = (databaseItemsWhereContentHasChanged.length + batchSize - 1) / batchSize;
ulong batchesProcessed = 0;
// For each batch of files to upload, upload the changed data to OneDrive
foreach (chunk; databaseItemsWhereContentHasChanged.chunks(batchSize)) {
uploadChangedLocalFileToOneDrive(chunk);
}
}
// Upload changed local files to OneDrive in parallel
void uploadChangedLocalFileToOneDrive(string[3][] array) {
foreach (i, localItemDetails; taskPool.parallel(array)) {
log.vdebug("Thread ", i, " Starting: ", Clock.currTime());
// These are the details of the item we need to upload
string changedItemParentId = localItemDetails[0];
string changedItemId = localItemDetails[1];
string localFilePath = localItemDetails[2];
// How much space is remaining on OneDrive
ulong remainingFreeSpace;
// Did the upload fail?
bool uploadFailed = false;
// Did we skip due to exceeding maximum allowed size?
bool skippedMaxSize = false;
// Did we skip to an exception error?
bool skippedExceptionError = false;
// Unfortunatly, we cant store an array of Item's ... so we have to re-query the DB again - unavoidable extra processing here
// This is because the Item[] has no other functions to allow is to parallel process those elements, so we have to use a string array as input to this function
Item dbItem;
itemDB.selectById(changedItemParentId, changedItemId, dbItem);
// Query the available space online
// This will update appConfig.quotaAvailable & appConfig.quotaRestricted values
remainingFreeSpace = getRemainingFreeSpace(dbItem.driveId);
// Get the file size
ulong thisFileSizeLocal = getSize(localFilePath);
ulong thisFileSizeFromDB = to!ulong(dbItem.size);
// remainingFreeSpace online includes the current file online
// we need to remove the online file (add back the existing file size) then take away the new local file size to get a new approximate value
ulong calculatedSpaceOnlinePostUpload = (remainingFreeSpace + thisFileSizeFromDB) - thisFileSizeLocal;
// Based on what we know, for this thread - can we safely upload this modified local file?
log.vdebug("This Thread Current Free Space Online: ", remainingFreeSpace);
log.vdebug("This Thread Calculated Free Space Online Post Upload: ", calculatedSpaceOnlinePostUpload);
JSONValue uploadResponse;
bool spaceAvailableOnline = false;
// If 'personal' accounts, if driveId == defaultDriveId, then we will have data - appConfig.quotaAvailable will be updated
// If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data - appConfig.quotaRestricted will be set as true
// If 'business' accounts, if driveId == defaultDriveId, then we will have data
// If 'business' accounts, if driveId != defaultDriveId, then we will have data, but it will be a 0 value - appConfig.quotaRestricted will be set as true
// What was the latest getRemainingFreeSpace() value?
if (appConfig.quotaAvailable) {
// Our query told us we have free space online .. if we upload this file, will we exceed space online - thus upload will fail during upload?
if (calculatedSpaceOnlinePostUpload > 0) {
// Based on this thread action, we beleive that there is space available online to upload - proceed
spaceAvailableOnline = true;
}
}
// Is quota being restricted?
if (appConfig.quotaRestricted) {
// Space available online is being restricted - so we have no way to really know if there is space available online
spaceAvailableOnline = true;
}
// Do we have space available or is space available being restricted (so we make the blind assumption that there is space available)
if (spaceAvailableOnline) {
// Does this file exceed the maximum file size to upload to OneDrive?
if (thisFileSizeLocal <= maxUploadFileSize) {
// Attempt to upload the modified file
// Error handling is in performModifiedFileUpload(), and the JSON that is responded with - will either be null or a valid JSON object containing the upload result
uploadResponse = performModifiedFileUpload(dbItem, localFilePath, thisFileSizeLocal);
// Evaluate the returned JSON uploadResponse
// If there was an error uploading the file, uploadResponse should be empty and invalid
if (uploadResponse.type() != JSONType.object){
uploadFailed = true;
skippedExceptionError = true;
}
} else {
// Skip file - too large
uploadFailed = true;
skippedMaxSize = true;
}
} else {
// Cant upload this file - no space available
uploadFailed = true;
}
// Did the upload fail?
if (uploadFailed) {
// Upload failed .. why?
// No space available online
if (!spaceAvailableOnline) {
log.logAndNotify("Skipping uploading modified file ", localFilePath, " due to insufficient free space available on OneDrive");
}
// File exceeds max allowed size
if (skippedMaxSize) {
log.logAndNotify("Skipping uploading this modified file as it exceeds the maximum size allowed by OneDrive: ", localFilePath);
}
// Generic message
if (skippedExceptionError) {
// normal failure message if API or exception error generated
log.logAndNotify("Uploading modified file ", localFilePath, " ... failed!");
}
} else {
// Upload was successful
log.logAndNotify("Uploading modified file ", localFilePath, " ... done.");
// Save JSON item in database
saveItem(uploadResponse);
if (!dryRun) {
// Check the integrity of the uploaded modified file
performUploadIntegrityValidationChecks(uploadResponse, localFilePath, thisFileSizeLocal);
// Update the date / time of the file online to match the local item
// Get the local file last modified time
SysTime localModifiedTime = timeLastModified(localFilePath).toUTC();
localModifiedTime.fracSecs = Duration.zero;
// Get the latest eTag, and use that
string etagFromUploadResponse = uploadResponse["eTag"].str;
// Attempt to update the online date time stamp based on our local data
uploadLastModifiedTime(dbItem.driveId, dbItem.id, localModifiedTime, etagFromUploadResponse);
}
}
log.vdebug("Thread ", i, " Finished: ", Clock.currTime());
} // end of 'foreach (i, localItemDetails; array.enumerate)'
}
// Perform the upload of a locally modified file to OneDrive
JSONValue performModifiedFileUpload(Item dbItem, string localFilePath, ulong thisFileSizeLocal) {
JSONValue uploadResponse;
OneDriveApi uploadFileOneDriveApiInstance;
uploadFileOneDriveApiInstance = new OneDriveApi(appConfig);
uploadFileOneDriveApiInstance.initialise();
// Is this a dry-run scenario?
if (!dryRun) {
// Do we use simpleUpload or create an upload session?
bool useSimpleUpload = false;
//if ((appConfig.accountType == "personal") && (thisFileSizeLocal <= sessionThresholdFileSize)) {
if (thisFileSizeLocal <= sessionThresholdFileSize) {
useSimpleUpload = true;
}
// We can only upload zero size files via simpleFileUpload regardless of account type
// Reference: https://github.com/OneDrive/onedrive-api-docs/issues/53
// Additionally, all files where file size is < 4MB should be uploaded by simpleUploadReplace - everything else should use a session to upload the modified file
if ((thisFileSizeLocal == 0) || (useSimpleUpload)) {
// Must use Simple Upload to replace the file online
try {
uploadResponse = uploadFileOneDriveApiInstance.simpleUploadReplace(localFilePath, dbItem.driveId, dbItem.id);
} catch (OneDriveException exception) {
string thisFunctionName = getFunctionName!({});
// 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(uploadFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to upload a modified file to OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
performModifiedFileUpload(dbItem, localFilePath, thisFileSizeLocal);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
}
} catch (FileException e) {
// filesystem error
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
} else {
// Configure JSONValue variables we use for a session upload
JSONValue currentOnlineData;
JSONValue uploadSessionData;
string currentETag;
// As this is a unique thread, the sessionFilePath for where we save the data needs to be unique
// The best way to do this is calculate the CRC32 of the file, and use this as the suffix of the session file we save
string threadUploadSessionFilePath = appConfig.uploadSessionFilePath ~ "." ~ computeCRC32(localFilePath);
// Get the absolute latest object details from online
try {
currentOnlineData = uploadFileOneDriveApiInstance.getPathDetailsByDriveId(dbItem.driveId, localFilePath);
} catch (OneDriveException exception) {
string thisFunctionName = getFunctionName!({});
// 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(uploadFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to obtain latest file details from OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
performModifiedFileUpload(dbItem, localFilePath, thisFileSizeLocal);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
}
}
// Was a valid JSON response provided?
if (currentOnlineData.type() == JSONType.object) {
// Does the response contain an eTag?
if (hasETag(currentOnlineData)) {
// Use the value returned from online
currentETag = currentOnlineData["eTag"].str;
} else {
// Use the database value
currentETag = dbItem.eTag;
}
} else {
// no valid JSON response
currentETag = dbItem.eTag;
}
// Create the Upload Session
try {
uploadSessionData = createSessionFileUpload(uploadFileOneDriveApiInstance, localFilePath, dbItem.driveId, dbItem.parentId, baseName(localFilePath), currentETag, threadUploadSessionFilePath);
} catch (OneDriveException exception) {
string thisFunctionName = getFunctionName!({});
// 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(uploadFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to create an upload session on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
performModifiedFileUpload(dbItem, localFilePath, thisFileSizeLocal);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
}
} catch (FileException e) {
writeln("DEBUG TO REMOVE: Modified file upload FileException Handling (Create the Upload Session)");
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
// Perform the Upload using the session
try {
uploadResponse = performSessionFileUpload(uploadFileOneDriveApiInstance, thisFileSizeLocal, uploadSessionData, threadUploadSessionFilePath);
} catch (OneDriveException exception) {
string thisFunctionName = getFunctionName!({});
// 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(uploadFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to upload a file via a session to OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
performModifiedFileUpload(dbItem, localFilePath, thisFileSizeLocal);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
}
} catch (FileException e) {
writeln("DEBUG TO REMOVE: Modified file upload FileException Handling (Perform the Upload using the session)");
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
}
} else {
// We are in a --dry-run scenario
uploadResponse = createFakeResponse(localFilePath);
}
// Debug Log the modified upload response
log.vdebug("Modified File Upload Response: ", uploadResponse);
// Shutdown the API instance
uploadFileOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(uploadFileOneDriveApiInstance);
// Return JSON
return uploadResponse;
}
// Query the OneDrive API using the provided driveId to get the latest quota details
ulong getRemainingFreeSpace(string driveId) {
// Get the quota details for this driveId, as this could have changed since we started the application - the user could have added / deleted data online, or purchased additional storage
// Quota details are ONLY available for the main default driveId, as the OneDrive API does not provide quota details for shared folders
JSONValue currentDriveQuota;
ulong remainingQuota;
try {
// Create a new OneDrive API instance
OneDriveApi getCurrentDriveQuotaApiInstance;
getCurrentDriveQuotaApiInstance = new OneDriveApi(appConfig);
getCurrentDriveQuotaApiInstance.initialise();
log.vdebug("Seeking available quota for this drive id: ", driveId);
currentDriveQuota = getCurrentDriveQuotaApiInstance.getDriveQuota(driveId);
// Shut this API instance down
getCurrentDriveQuotaApiInstance.shutdown();
// Free object and memory
object.destroy(getCurrentDriveQuotaApiInstance);
} catch (OneDriveException e) {
log.vdebug("currentDriveQuota = onedrive.getDriveQuota(driveId) generated a OneDriveException");
}
// validate that currentDriveQuota is a JSON value
if (currentDriveQuota.type() == JSONType.object) {
// Response from API contains valid data
// If 'personal' accounts, if driveId == defaultDriveId, then we will have data
// If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data
// If 'business' accounts, if driveId == defaultDriveId, then we will have data
// If 'business' accounts, if driveId != defaultDriveId, then we will have data, but it will be a 0 value
if ("quota" in currentDriveQuota){
if (driveId == appConfig.defaultDriveId) {
// We potentially have updated quota remaining details available
// However in some cases OneDrive Business configurations 'restrict' quota details thus is empty / blank / negative value / zero
if ("remaining" in currentDriveQuota["quota"]){
// We have valid quota remaining details returned for the provided drive id
remainingQuota = currentDriveQuota["quota"]["remaining"].integer;
if (remainingQuota <= 0) {
if (appConfig.accountType == "personal"){
// zero space available
log.error("ERROR: OneDrive account currently has zero space available. Please free up some space online or purchase additional space.");
remainingQuota = 0;
appConfig.quotaAvailable = false;
} else {
// zero space available is being reported, maybe being restricted?
log.error("WARNING: OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator.");
remainingQuota = 0;
appConfig.quotaRestricted = true;
}
}
}
} else {
// quota details returned, but for a drive id that is not ours
if ("remaining" in currentDriveQuota["quota"]){
// remaining is in the quota JSON response
if (currentDriveQuota["quota"]["remaining"].integer <= 0) {
// value returned is 0 or less than 0
log.vlog("OneDrive quota information is set at zero, as this is not our drive id, ignoring");
remainingQuota = 0;
appConfig.quotaRestricted = true;
}
}
}
} else {
// No quota details returned
if (driveId == appConfig.defaultDriveId) {
// no quota details returned for current drive id
log.error("ERROR: OneDrive quota information is missing. Potentially your OneDrive account currently has zero space available. Please free up some space online or purchase additional space.");
remainingQuota = 0;
appConfig.quotaRestricted = true;
} else {
// quota details not available
log.vdebug("WARNING: OneDrive quota information is being restricted as this is not our drive id.");
remainingQuota = 0;
appConfig.quotaRestricted = true;
}
}
}
// what was the determined available quota?
log.vdebug("Available quota: ", remainingQuota);
return remainingQuota;
}
// Perform a filesystem walk to uncover new data to upload to OneDrive
void scanLocalFilesystemPathForNewData(string path) {
// To improve logging output for this function, what is the 'logical path' we are scanning for file & folder differences?
string logPath;
if (path == ".") {
// get the configured sync_dir
logPath = buildNormalizedPath(appConfig.getValueString("sync_dir"));
} else {
// use what was passed in
if (!appConfig.getValueBool("monitor")) {
logPath = buildNormalizedPath(appConfig.getValueString("sync_dir")) ~ "/" ~ path;
} else {
logPath = path;
}
}
// Log the action that we are performing, however only if this is a directory
if (isDir(path)) {
if (!appConfig.surpressLoggingOutput) {
if (!cleanupLocalFiles) {
log.log("Scanning the local file system '", logPath, "' for new data to upload ...");
} else {
log.log("Scanning the local file system '", logPath, "' for data to cleanup ...");
}
}
}
auto startTime = Clock.currTime();
log.vdebug("Starting Filesystem Walk: ", startTime);
// Perform the filesystem walk of this path, building an array of new items to upload
scanPathForNewData(path);
// To finish off the processing items, this is needed to reflect this in the log
log.vdebug("------------------------------------------------------------------");
auto finishTime = Clock.currTime();
log.vdebug("Finished Filesystem Walk: ", finishTime);
auto elapsedTime = finishTime - startTime;
log.vdebug("Elapsed Time Filesystem Walk: ", elapsedTime);
// Upload new data that has been identified
// Are there any items to download post fetching the /delta data?
if (!newLocalFilesToUploadToOneDrive.empty) {
// There are elements to upload
log.vlog("New items to upload to OneDrive: ", newLocalFilesToUploadToOneDrive.length);
// How much data do we need to upload? This is important, as, we need to know how much data to determine if all the files can be uploaded
foreach (uploadFilePath; newLocalFilesToUploadToOneDrive) {
// validate that the path actually exists so that it can be counted
if (exists(uploadFilePath)) {
totalDataToUpload = totalDataToUpload + getSize(uploadFilePath);
}
}
// How many bytes to upload
if (totalDataToUpload < 1024) {
// Display as Bytes to upload
log.vlog("Total New Data to Upload: ", totalDataToUpload, " Bytes");
} else {
if ((totalDataToUpload > 1024) && (totalDataToUpload < 1048576)) {
// Display as KB to upload
log.vlog("Total New Data to Upload: ", (totalDataToUpload / 1024), " KB");
} else {
// Display as MB to upload
log.vlog("Total New Data to Upload: ", (totalDataToUpload / 1024 / 1024), " MB");
}
}
// How much space is available (Account Drive ID)
// The file, could be uploaded to a shared folder, which, we are not tracking how much free space is available there ...
log.vdebug("Current Available Space Online (Account Drive ID): ", (appConfig.remainingFreeSpace / 1024 / 1024), " MB");
// Perform the upload
uploadNewLocalFileItems();
// Cleanup array memory
newLocalFilesToUploadToOneDrive = [];
}
}
// Scan this path for new data
void scanPathForNewData(string path) {
ulong maxPathLength;
ulong pathWalkLength;
// Add this logging break to assist with what was checked for each path
if (path != ".") {
log.vdebug("------------------------------------------------------------------");
}
// https://support.microsoft.com/en-us/help/3125202/restrictions-and-limitations-when-you-sync-files-and-folders
// If the path is greater than allowed characters, then one drive will return a '400 - Bad Request'
// Need to ensure that the URI is encoded before the check is made:
// - 400 Character Limit for OneDrive Business / Office 365
// - 430 Character Limit for OneDrive Personal
// Configure maxPathLength based on account type
if (appConfig.accountType == "personal") {
// Personal Account
maxPathLength = 430;
} else {
// Business Account / Office365 / SharePoint
maxPathLength = 400;
}
// A short lived item that has already disappeared will cause an error - is the path still valid?
if (!exists(path)) {
log.log("Skipping item - path has disappeared: ", path);
return;
}
// Calculate the path length by walking the path and catch any UTF-8 sequence errors at the same time
// https://github.com/skilion/onedrive/issues/57
// https://github.com/abraunegg/onedrive/issues/487
// https://github.com/abraunegg/onedrive/issues/1192
try {
pathWalkLength = path.byGrapheme.walkLength;
} catch (std.utf.UTFException e) {
// Path contains characters which generate a UTF exception
log.logAndNotify("Skipping item - invalid UTF sequence: ", path);
log.vdebug(" Error Reason:", e.msg);
return;
}
// Is the path length is less than maxPathLength
if (pathWalkLength < maxPathLength) {
// Is this path unwanted
bool unwanted = false;
// First check of this item - if we are in a --dry-run scenario, we may have 'fake deleted' this path
// thus, the entries are not in the dry-run DB copy, thus, at this point the client thinks that this is an item to upload
// Check this 'path' for an entry in pathFakeDeletedArray - if it is there, this is unwanted
if (dryRun) {
// Is this path in the array of fake deleted items? If yes, return early, nothing else to do, save processing
if (canFind(pathFakeDeletedArray, path)) return;
}
// This not a Client Side Filtering check, nor a Microsoft Check, but is a sanity check that the path provided is UTF encoded correctly
// Check the std.encoding of the path against: Unicode 5.0, ASCII, ISO-8859-1, ISO-8859-2, WINDOWS-1250, WINDOWS-1251, WINDOWS-1252
if (!unwanted) {
if(!isValid(path)) {
// Path is not valid according to https://dlang.org/phobos/std_encoding.html
log.logAndNotify("Skipping item - invalid character encoding sequence: ", path);
unwanted = true;
}
}
// Check this path against the Client Side Filtering Rules
// - check_nosync
// - skip_dotfiles
// - skip_symlinks
// - skip_file
// - skip_dir
// - sync_list
// - skip_size
if (!unwanted) {
unwanted = checkPathAgainstClientSideFiltering(path);
}
// Check this path against the Microsoft Naming Conventions & Restristions
// - Microsoft OneDrive restriction and limitations about Windows naming files
// - Bad whitespace items
// - HTML ASCII Codes as part of file name
if (!unwanted) {
unwanted = checkPathAgainstMicrosoftNamingRestrictions(path);
}
if (!unwanted) {
// At this point, this path, we want to scan for new data as it is not excluded
if (isDir(path)) {
// Check if this path in the database
bool directoryFoundInDB = pathFoundInDatabase(path);
// Was the path found in the database?
if (!directoryFoundInDB) {
// Path not found in database when searching all drive id's
if (!cleanupLocalFiles) {
// --download-only --cleanup-local-files not used
// Create this directory on OneDrive so that we can upload files to it
createDirectoryOnline(path);
} else {
// we need to clean up this directory
log.log("Removing local directory as --download-only & --cleanup-local-files configured");
// Remove any children of this path if they still exist
// Resolve 'Directory not empty' error when deleting local files
try {
foreach (DirEntry child; dirEntries(path, SpanMode.depth, false)) {
// what sort of child is this?
if (isDir(child.name)) {
log.log("Removing local directory: ", child.name);
} else {
log.log("Removing local file: ", child.name);
}
// are we in a --dry-run scenario?
if (!dryRun) {
// No --dry-run ... process local delete
if (exists(child)) {
try {
attrIsDir(child.linkAttributes) ? rmdir(child.name) : remove(child.name);
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
}
}
}
// Remove the path now that it is empty of children
log.log("Removing local directory: ", path);
// are we in a --dry-run scenario?
if (!dryRun) {
// No --dry-run ... process local delete
try {
rmdirRecurse(path);
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
}
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
return;
}
}
}
// flag for if we are going traverse this path
bool skipFolderTraverse = false;
// Before we traverse this 'path', we need to make a last check to see if this was just excluded
if (appConfig.accountType == "business") {
// search businessSharedFoldersOnlineToSkip for this path
if (canFind(businessSharedFoldersOnlineToSkip, path)) {
// This path was skipped - why?
log.logAndNotify("Skipping item '", path, "' due to this path matching an existing online Business Shared Folder name");
skipFolderTraverse = true;
}
}
// Do we traverse this path?
if (!skipFolderTraverse) {
// Try and access this directory and any path below
try {
auto entries = dirEntries(path, SpanMode.shallow, false);
foreach (DirEntry entry; entries) {
string thisPath = entry.name;
scanPathForNewData(thisPath);
}
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
return;
}
}
} else {
// https://github.com/abraunegg/onedrive/issues/984
// path is not a directory, is it a valid file?
// pipes - whilst technically valid files, are not valid for this client
// prw-rw-r--. 1 user user 0 Jul 7 05:55 my_pipe
if (isFile(path)) {
// Path is a valid file, not a pipe
bool fileFoundInDB = pathFoundInDatabase(path);
// Was the file found in the database?
if (!fileFoundInDB) {
// File not found in database when searching all drive id's
// Do we upload the file or clean up the file?
if (!cleanupLocalFiles) {
// --download-only --cleanup-local-files not used
// Add this path as a file we need to upload
log.vdebug("OneDrive Client flagging to upload this file to OneDrive: ", path);
newLocalFilesToUploadToOneDrive ~= path;
} else {
// we need to clean up this file
log.log("Removing local file as --download-only & --cleanup-local-files configured");
// are we in a --dry-run scenario?
log.log("Removing local file: ", path);
if (!dryRun) {
// No --dry-run ... process local file delete
safeRemove(path);
}
}
}
} else {
// path is not a valid file
log.logAndNotify("Skipping item - item is not a valid file: ", path);
}
}
}
} else {
// This path was skipped - why?
log.logAndNotify("Skipping item '", path, "' due to the full path exceeding ", maxPathLength, " characters (Microsoft OneDrive limitation)");
}
}
// Handle a single file inotify trigger when using --monitor
void handleLocalFileTrigger(string localFilePath) {
// Is this path a new file or an existing one?
// Normally we would use pathFoundInDatabase() to calculate, but we need 'databaseItem' as well if the item is in the database
Item databaseItem;
bool fileFoundInDB = false;
string[3][] modifiedItemToUpload;
foreach (driveId; driveIDsArray) {
if (itemDB.selectByPath(localFilePath, driveId, databaseItem)) {
fileFoundInDB = true;
}
}
// Was the file found in the database?
if (!fileFoundInDB) {
// This is a new file
scanLocalFilesystemPathForNewData(localFilePath);
} else {
// This is a modified file, needs to be handled as such
modifiedItemToUpload ~= [databaseItem.driveId, databaseItem.id, localFilePath];
uploadChangedLocalFileToOneDrive(modifiedItemToUpload);
}
}
// Query the database to determine if this path is within the existing database
bool pathFoundInDatabase(string searchPath) {
// Check if this path in the database
Item databaseItem;
bool pathFoundInDB = false;
foreach (driveId; driveIDsArray) {
if (itemDB.selectByPath(searchPath, driveId, databaseItem)) {
pathFoundInDB = true;
}
}
return pathFoundInDB;
}
// Create a new directory online on OneDrive
// - Test if we can get the parent path details from the database, otherwise we need to search online
// for the path flow and create the folder that way
void createDirectoryOnline(string thisNewPathToCreate) {
log.log("OneDrive Client requested to create this directory online: ", thisNewPathToCreate);
Item parentItem;
JSONValue onlinePathData;
// Create a new API Instance for this thread and initialise it
OneDriveApi createDirectoryOnlineOneDriveApiInstance;
createDirectoryOnlineOneDriveApiInstance = new OneDriveApi(appConfig);
createDirectoryOnlineOneDriveApiInstance.initialise();
// What parent path to use?
string parentPath = dirName(thisNewPathToCreate); // will be either . or something else
// Configure the parentItem by if this is the account 'root' use the root details, or search the database for the parent details
if (parentPath == ".") {
// Parent path is '.' which is the account root
// Use client defaults
parentItem.driveId = appConfig.defaultDriveId; // Should give something like 12345abcde1234a1
parentItem.id = appConfig.defaultRootId; // Should give something like 12345ABCDE1234A1!101
} else {
// Query the parent path online
log.vdebug("Attempting to query Local Database for this parent path: ", parentPath);
// Attempt a 2 step process to work out where to create the directory
// Step 1: Query the DB first
// Step 2: Query online as last resort
// Step 1: Check if this path in the database
Item databaseItem;
bool pathFoundInDB = false;
foreach (driveId; driveIDsArray) {
if (itemDB.selectByPath(parentPath, driveId, databaseItem)) {
pathFoundInDB = true;
log.vdebug("databaseItem: ", databaseItem);
log.vdebug("pathFoundInDB: ", pathFoundInDB);
}
}
// Step 2: Query for the path online
if (!pathFoundInDB) {
// path not found in database
try {
log.vdebug("Attempting to query OneDrive Online for this parent path as path not found in local database: ", parentPath);
onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetails(parentPath);
// Save item to the database
saveItem(onlinePathData);
parentItem = makeItem(onlinePathData);
} catch (OneDriveException exception) {
if (exception.httpStatusCode == 404) {
// Parent does not exist ... need to create parent
log.vdebug("Parent path does not exist online: ", parentPath);
createDirectoryOnline(parentPath);
// no return here as we need to continue, but need to re-query the OneDrive API to get the right parental details now that they exist
onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetails(parentPath);
parentItem = makeItem(onlinePathData);
} else {
string thisFunctionName = getFunctionName!({});
// 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(createDirectoryOnlineOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to create a remote directory on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
createDirectoryOnline(thisNewPathToCreate);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
}
} else {
// parent path found in database ... use those details ...
parentItem = databaseItem;
}
}
// Make sure the full path does not exist online, this should generate a 404 response, to which then the folder will be created online
try {
// Try and query the OneDrive API for the path we need to create
log.vdebug("Attempting to query OneDrive for this path: ", thisNewPathToCreate);
if (parentItem.driveId == appConfig.defaultDriveId) {
// Use getPathDetailsByDriveId
onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, thisNewPathToCreate);
} else {
// If the parentItem.driveId is not our driveId - the path we are looking for will not be at the logical location that getPathDetailsByDriveId
// can use - as it will always return a 404 .. even if the path actually exists (which is the whole point of this test)
// Search the parentItem.driveId for any folder name match that we are going to create, then compare response JSON items with parentItem.id
// If no match, the folder we want to create does not exist at the location we are seeking to create it at, thus generate a 404
onlinePathData = createDirectoryOnlineOneDriveApiInstance.searchDriveForPath(parentItem.driveId, baseName(thisNewPathToCreate));
// Process the response from searching the drive
ulong responseCount = count(onlinePathData["value"].array);
if (responseCount > 0) {
// Search 'name' matches were found .. need to match these against parentItem.id
bool foundDirectoryOnline = false;
JSONValue foundDirectoryJSONItem;
// Items were returned .. but is one of these what we are looking for?
foreach (childJSON; onlinePathData["value"].array) {
// Is this item not a file?
if (!isFileItem(childJSON)) {
Item thisChildItem = makeItem(childJSON);
// Direct Match Check
if ((parentItem.id == thisChildItem.parentId) && (baseName(thisNewPathToCreate) == thisChildItem.name)) {
// High confidence that this child folder is a direct match we are trying to create and it already exists online
log.vdebug("Path we are searching for exists online: ", baseName(thisNewPathToCreate));
log.vdebug("childJSON: ", childJSON);
foundDirectoryOnline = true;
foundDirectoryJSONItem = childJSON;
break;
}
// Full Lower Case POSIX Match Check
string childAsLower = toLower(childJSON["name"].str);
string thisFolderNameAsLower = toLower(baseName(thisNewPathToCreate));
if (childAsLower == thisFolderNameAsLower) {
// This is a POSIX 'case in-sensitive match' .....
// Local item name has a 'case-insensitive match' to an existing item on OneDrive
foundDirectoryOnline = true;
foundDirectoryJSONItem = childJSON;
break;
}
}
}
if (foundDirectoryOnline) {
// Directory we are seeking was found online ...
onlinePathData = foundDirectoryJSONItem;
} else {
// No 'search item matches found' - raise a 404 so that the exception handling will take over to create the folder
throw new OneDriveException(404, "Name not found via search");
}
} else {
// No 'search item matches found' - raise a 404 so that the exception handling will take over to create the folder
throw new OneDriveException(404, "Name not found via search");
}
}
} catch (OneDriveException exception) {
if (exception.httpStatusCode == 404) {
// This is a good error - it means that the directory to create 100% does not exist online
// The directory was not found on the drive id we queried
log.vlog("The requested directory to create was not found on OneDrive - creating remote directory: ", thisNewPathToCreate);
// Build up the create directory request
JSONValue createDirectoryOnlineAPIResponse;
JSONValue newDriveItem = [
"name": JSONValue(baseName(thisNewPathToCreate)),
"folder": parseJSON("{}")
];
// Submit the creation request
// Fix for https://github.com/skilion/onedrive/issues/356
if (!dryRun) {
try {
// Attempt to create a new folder on the configured parent driveId & parent id
createDirectoryOnlineAPIResponse = createDirectoryOnlineOneDriveApiInstance.createById(parentItem.driveId, parentItem.id, newDriveItem);
// Is the response a valid JSON object - validation checking done in saveItem
saveItem(createDirectoryOnlineAPIResponse);
// Log that the directory was created
log.log("Successfully created the remote directory ", thisNewPathToCreate, " on OneDrive");
} catch (OneDriveException exception) {
if (exception.httpStatusCode == 409) {
// OneDrive API returned a 404 (above) to say the directory did not exist
// but when we attempted to create it, OneDrive responded that it now already exists
log.vlog("OneDrive reported that ", thisNewPathToCreate, " already exists .. OneDrive API race condition");
return;
} else {
// some other error from OneDrive was returned - display what it is
log.error("OneDrive generated an error when creating this path: ", thisNewPathToCreate);
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
return;
}
}
} else {
// Simulate a successful 'directory create' & save it to the dryRun database copy
log.log("Successfully created the remote directory ", thisNewPathToCreate, " on OneDrive");
// The simulated response has to pass 'makeItem' as part of saveItem
auto fakeResponse = createFakeResponse(thisNewPathToCreate);
// Save item to the database
saveItem(fakeResponse);
}
// Shutdown API instance
createDirectoryOnlineOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(createDirectoryOnlineOneDriveApiInstance);
return;
} else {
string thisFunctionName = getFunctionName!({});
// 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(createDirectoryOnlineOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to create a remote directory on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
createDirectoryOnline(thisNewPathToCreate);
} else {
// Re-Try
createDirectoryOnline(thisNewPathToCreate);
}
}
}
// If we get to this point - onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, thisNewPathToCreate) generated a 'valid' response ....
// This means that the folder potentially exists online .. which is odd .. as it should not have existed
if (onlinePathData.type() == JSONType.object) {
// A valid object was responded with
if (onlinePathData["name"].str == baseName(thisNewPathToCreate)) {
// OneDrive 'name' matches local path name
if (appConfig.accountType == "business") {
// We are a business account, this existing online folder, could be a Shared Online Folder and is the 'Add shortcut to My files' item
log.vdebug("onlinePathData: ", onlinePathData);
if (isItemRemote(onlinePathData)) {
// The folder is a remote item ... we do not want to create this ...
log.vdebug("Remote Existing Online Folder is most likely a OneDrive Shared Business Folder Link added by 'Add shortcut to My files'");
log.vdebug("We need to skip this path: ", thisNewPathToCreate);
// Add this path to businessSharedFoldersOnlineToSkip
businessSharedFoldersOnlineToSkip ~= [thisNewPathToCreate];
// no save to database, no online create
return;
}
}
log.vlog("The requested directory to create was found on OneDrive - skipping creating the directory: ", thisNewPathToCreate);
// Is the response a valid JSON object - validation checking done in saveItem
saveItem(onlinePathData);
return;
} else {
// Normally this would throw an error, however we cant use throw new posixException()
string msg = format("POSIX 'case-insensitive match' between '%s' (local) and '%s' (online) which violates the Microsoft OneDrive API namespace convention", baseName(thisNewPathToCreate), onlinePathData["name"].str);
displayPosixErrorMessage(msg);
log.error("ERROR: Requested directory to create has a 'case-insensitive match' to an existing directory on OneDrive online.");
log.error("ERROR: To resolve, rename this local directory: ", buildNormalizedPath(absolutePath(thisNewPathToCreate)));
log.log("Skipping creating this directory online due to 'case-insensitive match': ", thisNewPathToCreate);
// Add this path to posixViolationPaths
posixViolationPaths ~= [thisNewPathToCreate];
return;
}
} else {
// response is not valid JSON, an error was returned from OneDrive
log.error("ERROR: There was an error performing this operation on OneDrive");
log.error("ERROR: Increase logging verbosity to assist determining why.");
log.log("Skipping: ", buildNormalizedPath(absolutePath(thisNewPathToCreate)));
return;
}
}
// Test that the online name actually matches the requested local name
void performPosixTest(string localNameToCheck, string onlineName) {
// https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
// Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same,
// even though some file systems (such as a POSIX-compliant file system) may consider them as different.
// Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior.
if (localNameToCheck != onlineName) {
// POSIX Error
// Local item name has a 'case-insensitive match' to an existing item on OneDrive
throw new posixException(localNameToCheck, onlineName);
}
}
// Upload new file items as identified
void uploadNewLocalFileItems() {
// Lets deal with the new local items in a batch process
ulong batchSize = appConfig.concurrentThreads;
ulong batchCount = (newLocalFilesToUploadToOneDrive.length + batchSize - 1) / batchSize;
ulong batchesProcessed = 0;
foreach (chunk; newLocalFilesToUploadToOneDrive.chunks(batchSize)) {
uploadNewLocalFileItemsInParallel(chunk);
}
}
// Upload the file batches in parallel
void uploadNewLocalFileItemsInParallel(string[] array) {
foreach (i, fileToUpload; taskPool.parallel(array)) {
log.vdebug("Upload Thread ", i, " Starting: ", Clock.currTime());
uploadNewFile(fileToUpload);
log.vdebug("Upload Thread ", i, " Finished: ", Clock.currTime());
}
}
// Upload a new file to OneDrive
void uploadNewFile(string fileToUpload) {
// Debug for the moment
log.vdebug("fileToUpload: ", fileToUpload);
// These are the details of the item we need to upload
// How much space is remaining on OneDrive
ulong remainingFreeSpaceOnline;
// Did the upload fail?
bool uploadFailed = false;
// Did we skip due to exceeding maximum allowed size?
bool skippedMaxSize = false;
// Did we skip to an exception error?
bool skippedExceptionError = false;
// Is the parent path in the item database?
bool parentPathFoundInDB = false;
// Get this file size
ulong thisFileSize;
// Is there space available online
bool spaceAvailableOnline = false;
// Check the database for the parent path of fileToUpload
Item parentItem;
// What parent path to use?
string parentPath = dirName(fileToUpload); // will be either . or something else
if (parentPath == "."){
// Assume this is a new file in the users configured sync_dir root
// Use client defaults
parentItem.id = appConfig.defaultRootId; // Should give something like 12345ABCDE1234A1!101
parentItem.driveId = appConfig.defaultDriveId; // Should give something like 12345abcde1234a1
parentPathFoundInDB = true;
} else {
// Query the database using each of the driveId's we are using
foreach (driveId; driveIDsArray) {
// Query the database for this parent path using each driveId
Item dbResponse;
if(itemDB.selectByPath(parentPath, driveId, dbResponse)){
// parent path was found in the database
parentItem = dbResponse;
parentPathFoundInDB = true;
}
}
}
// If the parent path was found in the DB, to ensure we are uploading the the right location 'parentItem.driveId' must not be empty
if ((parentPathFoundInDB) && (parentItem.driveId.empty)) {
// switch to using defaultDriveId
log.log("parentItem.driveId is empty - using defaultDriveId for upload API calls");
parentItem.driveId = appConfig.defaultDriveId;
}
// Can we read the file - as a permissions issue or actual file corruption will cause a failure
// Resolves: https://github.com/abraunegg/onedrive/issues/113
if (readLocalFile(fileToUpload)) {
if (parentPathFoundInDB) {
// The local file can be read - so we can read it to attemtp to upload it in this thread
// Get the file size
thisFileSize = getSize(fileToUpload);
// Does this file exceed the maximum filesize for OneDrive
// Resolves: https://github.com/skilion/onedrive/issues/121 , https://github.com/skilion/onedrive/issues/294 , https://github.com/skilion/onedrive/issues/329
if (thisFileSize <= maxUploadFileSize) {
// Is there enough free space on OneDrive when we started this thread, to upload the file to OneDrive?
remainingFreeSpaceOnline = getRemainingFreeSpace(parentItem.driveId);
log.vdebug("Current Available Space Online (Upload Target Drive ID): ", (remainingFreeSpaceOnline / 1024 / 1024), " MB");
// When we compare the space online to the total we are trying to upload - is there space online?
ulong calculatedSpaceOnlinePostUpload = remainingFreeSpaceOnline - thisFileSize;
// If 'personal' accounts, if driveId == defaultDriveId, then we will have data - appConfig.quotaAvailable will be updated
// If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data - appConfig.quotaRestricted will be set as true
// If 'business' accounts, if driveId == defaultDriveId, then we will have data
// If 'business' accounts, if driveId != defaultDriveId, then we will have data, but it will be a 0 value - appConfig.quotaRestricted will be set as true
if (remainingFreeSpaceOnline > totalDataToUpload) {
// Space available
spaceAvailableOnline = true;
} else {
// we need to look more granular
// What was the latest getRemainingFreeSpace() value?
if (appConfig.quotaAvailable) {
// Our query told us we have free space online .. if we upload this file, will we exceed space online - thus upload will fail during upload?
if (calculatedSpaceOnlinePostUpload > 0) {
// Based on this thread action, we beleive that there is space available online to upload - proceed
spaceAvailableOnline = true;
}
}
}
// Is quota being restricted?
if (appConfig.quotaRestricted) {
// If the upload target drive is not our drive id, then it is a shared folder .. we need to print a space warning message
if (parentItem.driveId != appConfig.defaultDriveId) {
// Different message depending on account type
if (appConfig.accountType == "personal") {
log.vlog("WARNING: Shared Folder OneDrive quota information is being restricted or providing a zero value. Space available online cannot be guaranteed.");
} else {
log.vlog("WARNING: Shared Folder OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator.");
}
} else {
if (appConfig.accountType == "personal") {
log.vlog("WARNING: OneDrive quota information is being restricted or providing a zero value. Space available online cannot be guaranteed.");
} else {
log.vlog("WARNING: OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator.");
}
}
// Space available online is being restricted - so we have no way to really know if there is space available online
spaceAvailableOnline = true;
}
// Do we have space available or is space available being restricted (so we make the blind assumption that there is space available)
if (spaceAvailableOnline) {
// We need to check that this new local file does not exist on OneDrive
// Create a new API Instance for this thread and initialise it
OneDriveApi checkFileOneDriveApiInstance;
checkFileOneDriveApiInstance = new OneDriveApi(appConfig);
checkFileOneDriveApiInstance.initialise();
JSONValue fileDetailsFromOneDrive;
// https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
// Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same,
// even though some file systems (such as a POSIX-compliant file systems that Linux use) may consider them as different.
// Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior, OneDrive does not use this.
// In order to upload this file - this query HAS to respond as a 404 - Not Found
// Does this 'file' already exist on OneDrive?
try {
fileDetailsFromOneDrive = checkFileOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, fileToUpload);
// Portable Operating System Interface (POSIX) testing of JSON response from OneDrive API
performPosixTest(baseName(fileToUpload), fileDetailsFromOneDrive["name"].str);
// No 404 or otherwise was triggered, meaning that the file already exists online and passes the POSIX test ...
log.vdebug("fileDetailsFromOneDrive after exist online check: ", fileDetailsFromOneDrive);
// Does the data from online match our local file?
if (performUploadIntegrityValidationChecks(fileDetailsFromOneDrive, fileToUpload, thisFileSize)) {
// Save item to the database
saveItem(fileDetailsFromOneDrive);
}
} catch (OneDriveException exception) {
// If we get a 404 .. the file is not online .. this is what we want .. file does not exist online
if (exception.httpStatusCode == 404) {
// The file has been checked, client side filtering checked, does not exist online - we need to upload it
log.vdebug("fileDetailsFromOneDrive = checkFileOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, fileToUpload); generated a 404 - file does not exist online - must upload it");
uploadFailed = performNewFileUpload(parentItem, fileToUpload, thisFileSize);
} else {
string thisFunctionName = getFunctionName!({});
// 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(checkFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to validate file details on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
uploadNewFile(fileToUpload);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
} catch (posixException e) {
displayPosixErrorMessage(e.msg);
uploadFailed = true;
}
// Operations in this thread are done / complete - either upload was done or it failed
checkFileOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(checkFileOneDriveApiInstance);
} else {
// skip file upload - insufficent space to upload
log.log("Skipping uploading this new file as it exceeds the available free space on OneDrive: ", fileToUpload);
uploadFailed = true;
}
} else {
// Skip file upload - too large
log.log("Skipping uploading this new file as it exceeds the maximum size allowed by OneDrive: ", fileToUpload);
uploadFailed = true;
}
} else {
// why was the parent path not in the database?
if (canFind(posixViolationPaths, parentPath)) {
log.error("ERROR: POSIX 'case-insensitive match' for the parent path which violates the Microsoft OneDrive API namespace convention.");
} else {
log.error("ERROR: Parent path is not in the database or online.");
}
log.error("ERROR: Unable to upload this file: ", fileToUpload);
uploadFailed = true;
}
} else {
// Unable to read local file
log.log("Skipping uploading this file as it cannot be read (file permissions or file corruption): ", fileToUpload);
uploadFailed = true;
}
// Upload success or failure?
if (uploadFailed) {
// Need to add this to fileUploadFailures to capture at the end
fileUploadFailures ~= fileToUpload;
}
}
// Perform the actual upload to OneDrive
bool performNewFileUpload(Item parentItem, string fileToUpload, ulong thisFileSize) {
// Assume that by default the upload fails
bool uploadFailed = true;
// OneDrive API Upload Response
JSONValue uploadResponse;
// Create the OneDriveAPI Upload Instance
OneDriveApi uploadFileOneDriveApiInstance;
uploadFileOneDriveApiInstance = new OneDriveApi(appConfig);
uploadFileOneDriveApiInstance.initialise();
// Calculate upload speed
auto uploadStartTime = Clock.currTime();
// Is this a dry-run scenario?
if (!dryRun) {
// Not a dry-run situation
// Do we use simpleUpload or create an upload session?
bool useSimpleUpload = false;
if (thisFileSize <= sessionThresholdFileSize) {
useSimpleUpload = true;
}
// We can only upload zero size files via simpleFileUpload regardless of account type
// Reference: https://github.com/OneDrive/onedrive-api-docs/issues/53
// Additionally, only where file size is < 4MB should be uploaded by simpleUpload - everything else should use a session to upload
if ((thisFileSize == 0) || (useSimpleUpload)) {
try {
// Attempt to upload the zero byte file using simpleUpload for all account types
uploadResponse = uploadFileOneDriveApiInstance.simpleUpload(fileToUpload, parentItem.driveId, parentItem.id, baseName(fileToUpload));
uploadFailed = false;
log.log("Uploading new file ", fileToUpload, " ... done.");
// Shutdown the API
uploadFileOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(uploadFileOneDriveApiInstance);
} catch (OneDriveException exception) {
// An error was responded with - what was it
string thisFunctionName = getFunctionName!({});
// 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(uploadFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to upload a new file to OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
performNewFileUpload(parentItem, fileToUpload, thisFileSize);
// Return upload status
return uploadFailed;
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
log.log("Uploading new file ", fileToUpload, " ... failed.");
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
} catch (FileException e) {
// display the error message
log.log("Uploading new file ", fileToUpload, " ... failed.");
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
} else {
// Session Upload for this criteria:
// - Personal Account and file size > 4MB
// - All Business | Office365 | SharePoint files > 0 bytes
JSONValue uploadSessionData;
// As this is a unique thread, the sessionFilePath for where we save the data needs to be unique
// The best way to do this is calculate the CRC32 of the file, and use this as the suffix of the session file we save
string threadUploadSessionFilePath = appConfig.uploadSessionFilePath ~ "." ~ computeCRC32(fileToUpload);
// Attempt to upload the > 4MB file using an upload session for all account types
try {
// Create the Upload Session
uploadSessionData = createSessionFileUpload(uploadFileOneDriveApiInstance, fileToUpload, parentItem.driveId, parentItem.id, baseName(fileToUpload), null, threadUploadSessionFilePath);
} catch (OneDriveException exception) {
// An error was responded with - what was it
string thisFunctionName = getFunctionName!({});
// 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(uploadFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to create an upload session on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
performNewFileUpload(parentItem, fileToUpload, thisFileSize);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
log.log("Uploading new file ", fileToUpload, " ... failed.");
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
} catch (FileException e) {
// display the error message
log.log("Uploading new file ", fileToUpload, " ... failed.");
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
// Do we have a valid session URL that we can use ?
if (uploadSessionData.type() == JSONType.object) {
// This is a valid JSON object
bool sessionDataValid = true;
// Validate that we have the following items which we need
if (!hasUploadURL(uploadSessionData)) {
sessionDataValid = false;
log.vdebug("Session data missing 'uploadUrl'");
}
if (!hasNextExpectedRanges(uploadSessionData)) {
sessionDataValid = false;
log.vdebug("Session data missing 'nextExpectedRanges'");
}
if (!hasLocalPath(uploadSessionData)) {
sessionDataValid = false;
log.vdebug("Session data missing 'localPath'");
}
if (sessionDataValid) {
// We have a valid Upload Session Data we can use
try {
// Try and perform the upload session
uploadResponse = performSessionFileUpload(uploadFileOneDriveApiInstance, thisFileSize, uploadSessionData, threadUploadSessionFilePath);
if (uploadResponse.type() == JSONType.object) {
uploadFailed = false;
log.log("Uploading new file ", fileToUpload, " ... done.");
} else {
log.log("Uploading new file ", fileToUpload, " ... failed.");
uploadFailed = true;
}
} catch (OneDriveException exception) {
string thisFunctionName = getFunctionName!({});
// 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(uploadFileOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to upload a new file via a session to OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
performNewFileUpload(parentItem, fileToUpload, thisFileSize);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
log.log("Uploading new file ", fileToUpload, " ... failed.");
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
} else {
// No Upload URL or nextExpectedRanges or localPath .. not a valid JSON we can use
log.vlog("Session data is missing required elements to perform a session upload.");
log.log("Uploading new file ", fileToUpload, " ... failed.");
}
} else {
// Create session Upload URL failed
log.log("Uploading new file ", fileToUpload, " ... failed.");
}
}
} else {
// We are in a --dry-run scenario
uploadResponse = createFakeResponse(fileToUpload);
uploadFailed = false;
log.logAndNotify("Uploading new file ", fileToUpload, " ... done.");
}
// Upload has finished
auto uploadFinishTime = Clock.currTime();
// If no upload failure, calculate metrics, perform integrity validation
if (!uploadFailed) {
// Upload did not fail ...
auto uploadDuration = uploadFinishTime - uploadStartTime;
log.vdebug("File Size: ", thisFileSize, " Bytes");
log.vdebug("Upload Duration: ", (uploadDuration.total!"msecs"/1e3), " Seconds");
auto uploadSpeed = (thisFileSize / (uploadDuration.total!"msecs"/1e3)/ 1024 / 1024);
log.vdebug("Upload Speed: ", uploadSpeed, " Mbps (approx)");
// OK as the upload did not fail, we need to save the response from OneDrive, but it has to be a valid JSON response
if (uploadResponse.type() == JSONType.object) {
// check if the path still exists locally before we try to set the file times online - as short lived files, whilst we uploaded it - it may not exist locally aready
if (exists(fileToUpload)) {
if (!dryRun) {
// Check the integrity of the uploaded file, if the local file still exists
performUploadIntegrityValidationChecks(uploadResponse, fileToUpload, thisFileSize);
// Update the file modified time on OneDrive and save item details to database
// Update the item's metadata on OneDrive
SysTime mtime = timeLastModified(fileToUpload).toUTC();
mtime.fracSecs = Duration.zero;
string newFileId = uploadResponse["id"].str;
string newFileETag = uploadResponse["eTag"].str;
// Attempt to update the online date time stamp based on our local data
uploadLastModifiedTime(parentItem.driveId, newFileId, mtime, newFileETag);
}
} else {
// will be removed in different event!
log.log("File disappeared locally after upload: ", fileToUpload);
}
} else {
// Log that an invalid JSON object was returned
log.vdebug("uploadFileOneDriveApiInstance.simpleUpload or session.upload call returned an invalid JSON Object from the OneDrive API");
}
}
// Return upload status
return uploadFailed;
}
// Create the OneDrive Upload Session
JSONValue createSessionFileUpload(OneDriveApi activeOneDriveApiInstance, string fileToUpload, string parentDriveId, string parentId, string filename, string eTag, string threadUploadSessionFilePath) {
// Upload file via a OneDrive API session
JSONValue uploadSession;
// Calculate modification time
SysTime localFileLastModifiedTime = timeLastModified(fileToUpload).toUTC();
localFileLastModifiedTime.fracSecs = Duration.zero;
// Construct the fileSystemInfo JSON component needed to create the Upload Session
JSONValue fileSystemInfo = [
"item": JSONValue([
"@microsoft.graph.conflictBehavior": JSONValue("replace"),
"fileSystemInfo": JSONValue([
"lastModifiedDateTime": localFileLastModifiedTime.toISOExtString()
])
])
];
// Try to create the upload session for this file
uploadSession = activeOneDriveApiInstance.createUploadSession(parentDriveId, parentId, filename, eTag, fileSystemInfo);
if (uploadSession.type() == JSONType.object) {
// a valid session object was created
if ("uploadUrl" in uploadSession) {
// Add the file path we are uploading to this JSON Session Data
uploadSession["localPath"] = fileToUpload;
// Save this session
saveSessionFile(threadUploadSessionFilePath, uploadSession);
}
} else {
// no valid session was created
log.vlog("Creation of OneDrive API Upload Session failed.");
// return upload() will return a JSONValue response, create an empty JSONValue response to return
uploadSession = null;
}
// Return the JSON
return uploadSession;
}
// Save the session upload data
void saveSessionFile(string threadUploadSessionFilePath, JSONValue uploadSessionData) {
try {
std.file.write(threadUploadSessionFilePath, uploadSessionData.toString());
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
}
}
// Perform the upload of file via the Upload Session that was created
JSONValue performSessionFileUpload(OneDriveApi activeOneDriveApiInstance, ulong thisFileSize, JSONValue uploadSessionData, string threadUploadSessionFilePath) {
// Response for upload
JSONValue uploadResponse;
// Session JSON needs to contain valid elements
// Get the offset details
ulong fragmentSize = 10 * 2^^20; // 10 MiB
ulong fragmentCount = 0;
ulong fragSize = 0;
ulong offset = uploadSessionData["nextExpectedRanges"][0].str.splitter('-').front.to!ulong;
size_t iteration = (roundTo!int(double(thisFileSize)/double(fragmentSize)))+1;
Progress p = new Progress(iteration);
p.title = "Uploading";
// Initialise the download bar at 0%
p.next();
// Start the session upload using the active API instance for this thread
while (true) {
fragmentCount++;
log.vdebugNewLine("Fragment: ", fragmentCount, " of ", iteration);
p.next();
log.vdebugNewLine("fragmentSize: ", fragmentSize, "offset: ", offset, " thisFileSize: ", thisFileSize );
fragSize = fragmentSize < thisFileSize - offset ? fragmentSize : thisFileSize - offset;
log.vdebugNewLine("Using fragSize: ", fragSize);
// fragSize must not be a negative value
if (fragSize < 0) {
// Session upload will fail
// not a JSON object - fragment upload failed
log.vlog("File upload session failed - invalid calculation of fragment size");
if (exists(threadUploadSessionFilePath)) {
remove(threadUploadSessionFilePath);
}
// set uploadResponse to null as error
uploadResponse = null;
return uploadResponse;
}
// If the resume upload fails, we need to check for a return code here
try {
uploadResponse = activeOneDriveApiInstance.uploadFragment(
uploadSessionData["uploadUrl"].str,
uploadSessionData["localPath"].str,
offset,
fragSize,
thisFileSize
);
} catch (OneDriveException exception) {
// if a 100 uploadResponse is generated, continue
if (exception.httpStatusCode == 100) {
continue;
}
// There was an error uploadResponse from OneDrive when uploading the file fragment
// Handle transient errors:
// 408 - Request Time Out
// 429 - Too Many Requests
// 503 - Service Unavailable
// 504 - Gateway Timeout
// HTTP request returned status code 408,429,503,504
if ((exception.httpStatusCode == 408) || (exception.httpStatusCode == 429) || (exception.httpStatusCode == 503) || (exception.httpStatusCode == 504)) {
// Handle 'HTTP request returned status code 429 (Too Many Requests)' first
log.vdebug("Fragment upload failed - received throttle request uploadResponse from OneDrive");
if (exception.httpStatusCode == 429) {
auto retryAfterValue = activeOneDriveApiInstance.getRetryAfterValue();
log.vdebug("Using Retry-After Value = ", retryAfterValue);
// Sleep thread as per request
log.log("\nThread sleeping due to 'HTTP request returned status code 429' - The request has been throttled");
log.log("Sleeping for ", retryAfterValue, " seconds");
Thread.sleep(dur!"seconds"(retryAfterValue));
log.log("Retrying fragment upload");
} else {
// Handle 408, 503 and 504
auto errorArray = splitLines(exception.msg);
auto retryAfterValue = 30;
log.log("\nThread sleeping due to '", errorArray[0], "' - retrying applicable request in 30 seconds");
log.log("Sleeping for ", retryAfterValue, " seconds");
Thread.sleep(dur!"seconds"(retryAfterValue));
log.log("Retrying fragment upload");
}
} else {
// insert a new line as well, so that the below error is inserted on the console in the right location
log.vlog("\nFragment upload failed - received an exception response from OneDrive API");
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
// retry fragment upload in case error is transient
log.vlog("Retrying fragment upload");
}
try {
uploadResponse = activeOneDriveApiInstance.uploadFragment(
uploadSessionData["uploadUrl"].str,
uploadSessionData["localPath"].str,
offset,
fragSize,
thisFileSize
);
} catch (OneDriveException e) {
// OneDrive threw another error on retry
log.vlog("Retry to upload fragment failed");
// display what the error is
displayOneDriveErrorMessage(e.msg, getFunctionName!({}));
// set uploadResponse to null as the fragment upload was in error twice
uploadResponse = null;
} catch (std.exception.ErrnoException e) {
// There was a file system error - display the error message
displayFileSystemErrorMessage(e.msg, getFunctionName!({}));
return uploadResponse;
}
}
// was the fragment uploaded without issue?
if (uploadResponse.type() == JSONType.object){
offset += fragmentSize;
if (offset >= thisFileSize) break;
// update the uploadSessionData details
uploadSessionData["expirationDateTime"] = uploadResponse["expirationDateTime"];
uploadSessionData["nextExpectedRanges"] = uploadResponse["nextExpectedRanges"];
saveSessionFile(threadUploadSessionFilePath, uploadSessionData);
} else {
// not a JSON object - fragment upload failed
log.vlog("File upload session failed - invalid response from OneDrive API");
if (exists(threadUploadSessionFilePath)) {
remove(threadUploadSessionFilePath);
}
// set uploadResponse to null as error
uploadResponse = null;
return uploadResponse;
}
}
// upload complete
p.next();
writeln();
if (exists(threadUploadSessionFilePath)) {
remove(threadUploadSessionFilePath);
}
// Return the session upload response
return uploadResponse;
}
// Delete an item on OneDrive
void uploadDeletedItem(Item itemToDelete, string path) {
// Are we in a situation where we HAVE to keep the data online - do not delete the remote object
if (noRemoteDelete) {
if ((itemToDelete.type == ItemType.dir)) {
// Do not process remote directory delete
log.vlog("Skipping remote directory delete as --upload-only & --no-remote-delete configured");
} else {
// Do not process remote file delete
log.vlog("Skipping remote file delete as --upload-only & --no-remote-delete configured");
}
} else {
// Process the delete - delete the object online
log.log("Deleting item from OneDrive: ", path);
bool flagAsBigDelete = false;
Item[] children;
ulong itemsToDelete;
if ((itemToDelete.type == ItemType.dir)) {
// Query the database - how many objects will this remove?
children = getChildren(itemToDelete.driveId, itemToDelete.id);
// Count the returned items + the original item (1)
itemsToDelete = count(children) + 1;
log.vdebug("Number of items online to delete: ", itemsToDelete);
} else {
itemsToDelete = 1;
}
// A local delete of a file|folder when using --monitor will issue a inotify event, which will trigger the local & remote data immediately be deleted
// The user may also be --sync process, so we are checking if something was deleted between application use
if (itemsToDelete >= appConfig.getValueLong("classify_as_big_delete")) {
// A big delete has been detected
flagAsBigDelete = true;
if (!appConfig.getValueBool("force")) {
log.error("ERROR: An attempt to remove a large volume of data from OneDrive has been detected. Exiting client to preserve data on OneDrive");
log.error("ERROR: To delete a large volume of data use --force or increase the config value 'classify_as_big_delete' to a larger value");
// Must exit here to preserve data on online
exit(-1);
}
}
// Are we in a --dry-run scenario?
if (!dryRun) {
// We are not in a dry run scenario
log.vdebug("itemToDelete: ", itemToDelete);
// Create new OneDrive API Instance
OneDriveApi uploadDeletedItemOneDriveApiInstance;
uploadDeletedItemOneDriveApiInstance = new OneDriveApi(appConfig);
uploadDeletedItemOneDriveApiInstance.initialise();
// what item are we trying to delete?
log.vdebug("Attempting to delete this single item id: ", itemToDelete.id, " from drive: ", itemToDelete.driveId);
try {
// perform the delete via the default OneDrive API instance
uploadDeletedItemOneDriveApiInstance.deleteById(itemToDelete.driveId, itemToDelete.id);
// Shutdown API
uploadDeletedItemOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(uploadDeletedItemOneDriveApiInstance);
} catch (OneDriveException e) {
if (e.httpStatusCode == 404) {
// item.id, item.eTag could not be found on the specified driveId
log.vlog("OneDrive reported: The resource could not be found to be deleted.");
}
}
// Delete the reference in the local database
itemDB.deleteById(itemToDelete.driveId, itemToDelete.id);
if (itemToDelete.remoteId != null) {
// If the item is a remote item, delete the reference in the local database
itemDB.deleteById(itemToDelete.remoteDriveId, itemToDelete.remoteId);
}
} else {
// log that this is a dry-run activity
log.log("dry run - no delete activity");
}
}
}
// Get the children of an item id from the database
Item[] getChildren(string driveId, string id) {
Item[] children;
children ~= itemDB.selectChildren(driveId, id);
foreach (Item child; children) {
if (child.type != ItemType.file) {
// recursively get the children of this child
children ~= getChildren(child.driveId, child.id);
}
}
return children;
}
// Perform a 'reverse' delete of all child objects on OneDrive
void performReverseDeletionOfOneDriveItems(Item[] children, Item itemToDelete) {
// Log what is happening
log.vdebug("Attempting a reverse delete of all child objects from OneDrive");
// Create a new API Instance for this thread and initialise it
OneDriveApi performReverseDeletionOneDriveApiInstance;
performReverseDeletionOneDriveApiInstance = new OneDriveApi(appConfig);
performReverseDeletionOneDriveApiInstance.initialise();
foreach_reverse (Item child; children) {
// Log the action
log.vdebug("Attempting to delete this child item id: ", child.id, " from drive: ", child.driveId);
// perform the delete via the default OneDrive API instance
performReverseDeletionOneDriveApiInstance.deleteById(child.driveId, child.id, child.eTag);
// delete the child reference in the local database
itemDB.deleteById(child.driveId, child.id);
}
// Log the action
log.vdebug("Attempting to delete this parent item id: ", itemToDelete.id, " from drive: ", itemToDelete.driveId);
// Perform the delete via the default OneDrive API instance
performReverseDeletionOneDriveApiInstance.deleteById(itemToDelete.driveId, itemToDelete.id, itemToDelete.eTag);
// Shutdown API instance
performReverseDeletionOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(performReverseDeletionOneDriveApiInstance);
}
// Create a fake OneDrive response suitable for use with saveItem
JSONValue createFakeResponse(const(string) path) {
import std.digest.sha;
// Generate a simulated JSON response which can be used
// At a minimum we need:
// 1. eTag
// 2. cTag
// 3. fileSystemInfo
// 4. file or folder. if file, hash of file
// 5. id
// 6. name
// 7. parent reference
string fakeDriveId = appConfig.defaultDriveId;
string fakeRootId = appConfig.defaultRootId;
SysTime mtime = timeLastModified(path).toUTC();
// Need to update the 'fakeDriveId' & 'fakeRootId' with elements from the --dry-run database
// Otherwise some calls to validate objects will fail as the actual driveId being used is invalid
string parentPath = dirName(path);
Item databaseItem;
if (parentPath != ".") {
// Not a 'root' parent
// For each driveid in the existing driveIDsArray
foreach (searchDriveId; driveIDsArray) {
log.vdebug("FakeResponse: searching database for: ", searchDriveId, " ", parentPath);
if (itemDB.selectByPath(parentPath, searchDriveId, databaseItem)) {
log.vdebug("FakeResponse: Found Database Item: ", databaseItem);
fakeDriveId = databaseItem.driveId;
fakeRootId = databaseItem.id;
}
}
}
// real id / eTag / cTag are different format for personal / business account
auto sha1 = new SHA1Digest();
ubyte[] fakedOneDriveItemValues = sha1.digest(path);
JSONValue fakeResponse;
if (isDir(path)) {
// path is a directory
fakeResponse = [
"id": JSONValue(toHexString(fakedOneDriveItemValues)),
"cTag": JSONValue(toHexString(fakedOneDriveItemValues)),
"eTag": JSONValue(toHexString(fakedOneDriveItemValues)),
"fileSystemInfo": JSONValue([
"createdDateTime": mtime.toISOExtString(),
"lastModifiedDateTime": mtime.toISOExtString()
]),
"name": JSONValue(baseName(path)),
"parentReference": JSONValue([
"driveId": JSONValue(fakeDriveId),
"driveType": JSONValue(appConfig.accountType),
"id": JSONValue(fakeRootId)
]),
"folder": JSONValue("")
];
} else {
// path is a file
// compute file hash - both business and personal responses use quickXorHash
string quickXorHash = computeQuickXorHash(path);
fakeResponse = [
"id": JSONValue(toHexString(fakedOneDriveItemValues)),
"cTag": JSONValue(toHexString(fakedOneDriveItemValues)),
"eTag": JSONValue(toHexString(fakedOneDriveItemValues)),
"fileSystemInfo": JSONValue([
"createdDateTime": mtime.toISOExtString(),
"lastModifiedDateTime": mtime.toISOExtString()
]),
"name": JSONValue(baseName(path)),
"parentReference": JSONValue([
"driveId": JSONValue(fakeDriveId),
"driveType": JSONValue(appConfig.accountType),
"id": JSONValue(fakeRootId)
]),
"file": JSONValue([
"hashes":JSONValue([
"quickXorHash": JSONValue(quickXorHash)
])
])
];
}
log.vdebug("Generated Fake OneDrive Response: ", fakeResponse);
return fakeResponse;
}
// Save JSON item details into the item database
void saveItem(JSONValue jsonItem) {
// jsonItem has to be a valid object
if (jsonItem.type() == JSONType.object) {
// Check if the response JSON has an 'id', otherwise makeItem() fails with 'Key not found: id'
if (hasId(jsonItem)) {
// Are we in a --upload-only & --remove-source-files scenario?
// We do not want to add the item to the database in this situation as there is no local reference to the file post file deletion
// If the item is a directory, we need to add this to the DB, if this is a file, we dont add this, the parent path is not in DB, thus any new files in this directory are not added
if ((uploadOnly) && (localDeleteAfterUpload) && (isItemFile(jsonItem))) {
// Log that we skipping adding item to the local DB and the reason why
log.vdebug("Skipping adding to database as --upload-only & --remove-source-files configured");
} else {
// What is the JSON item we are trying to create a DB record with?
log.vdebug("saveItem - creating DB item from this JSON: ", jsonItem);
// Takes a JSON input and formats to an item which can be used by the database
Item item = makeItem(jsonItem);
// Is this JSON item a 'root' item?
if ((isItemRoot(jsonItem)) && (item.name == "root")) {
log.vdebug("Updating DB Item object with correct values as this is a 'root' object");
item.parentId = null; // ensures that this database entry has no parent
// Check for parentReference
if (hasParentReference(jsonItem)) {
// Set the correct item.driveId
log.vdebug("ROOT JSON Item HAS parentReference .... setting item.driveId = jsonItem['parentReference']['driveId'].str");
item.driveId = jsonItem["parentReference"]["driveId"].str;
}
// We only should be adding our account 'root' to the database, not shared folder 'root' items
if (item.driveId != appConfig.defaultDriveId) {
// Shared Folder drive 'root' object .. we dont want this item
log.vdebug("NOT adding 'remote root' object to database: ", item);
return;
}
}
// Add to the local database
log.vdebug("Adding to database: ", item);
itemDB.upsert(item);
// If we have a remote drive ID, add this to our list of known drive id's
if (!item.remoteDriveId.empty) {
// Keep the driveIDsArray with unique entries only
if (!canFind(driveIDsArray, item.remoteDriveId)) {
// Add this drive id to the array to search with
driveIDsArray ~= item.remoteDriveId;
}
}
}
} else {
// log error
log.error("ERROR: OneDrive response missing required 'id' element");
log.error("ERROR: ", jsonItem);
}
} else {
// log error
log.error("ERROR: An error was returned from OneDrive and the resulting response is not a valid JSON object");
log.error("ERROR: Increase logging verbosity to assist determining why.");
}
}
// Wrapper function for makeDatabaseItem so we can check to ensure that the item has the required hashes
Item makeItem(JSONValue onedriveJSONItem) {
// Make the DB Item from the JSON data provided
Item newDatabaseItem = makeDatabaseItem(onedriveJSONItem);
// Is this a 'file' item that has not been deleted? Deleted items have no hash
if ((newDatabaseItem.type == ItemType.file) && (!isItemDeleted(onedriveJSONItem))) {
// Does this item have a file size attribute?
if (hasFileSize(onedriveJSONItem)) {
// Is the file size greater than 0?
if (onedriveJSONItem["size"].integer > 0) {
// Does the DB item have any hashes as per the API provided JSON data?
if ((newDatabaseItem.quickXorHash.empty) && (newDatabaseItem.sha256Hash.empty)) {
// Odd .. there is no hash for this item .. why is that?
// Is there a 'file' JSON element?
if ("file" in onedriveJSONItem) {
// Microsoft OneDrive OneNote objects will report as files but have 'application/msonenote' and 'application/octet-stream' as mime types
if ((isMicrosoftOneNoteMimeType1(onedriveJSONItem)) || (isMicrosoftOneNoteMimeType2(onedriveJSONItem))) {
// Debug log output that this is a potential OneNote object
log.vdebug("This item is potentially an associated Microsoft OneNote Object Item");
} else {
// Not a Microsoft OneNote Mime Type Object ..
string apiWarningMessage = "WARNING: OneDrive API inconsistency - this file does not have any hash: ";
// This is computationally expensive .. but we are only doing this if there are no hashses provided
bool parentInDatabase = itemDB.idInLocalDatabase(newDatabaseItem.driveId, newDatabaseItem.parentId);
// Is the parent id in the database?
if (parentInDatabase) {
// This is again computationally expensive .. calculate this item path to advise the user the actual path of this item that has no hash
string newItemPath = computeItemPath(newDatabaseItem.driveId, newDatabaseItem.parentId) ~ "/" ~ newDatabaseItem.name;
log.log(apiWarningMessage, newItemPath);
} else {
// Parent is not in the database .. why?
// Check if the parent item had been skipped ..
if (newDatabaseItem.parentId in skippedItems) {
log.vdebug(apiWarningMessage, "newDatabaseItem.parentId listed within skippedItems");
} else {
// Use the item ID .. there is no other reference available, parent is not being skipped, so we should have been able to calculate this - but we could not
log.log(apiWarningMessage, newDatabaseItem.id);
}
}
}
}
}
} else {
// zero file size
log.vdebug("This item file is zero size - potentially no hash provided by the OneDrive API");
}
}
}
// Return the new database item
return newDatabaseItem;
}
// Print the fileDownloadFailures and fileUploadFailures arrays if they are not empty
void displaySyncFailures() {
// Were there any file download failures?
if (!fileDownloadFailures.empty) {
// There are download failures ...
log.log("\nFailed items to download from OneDrive: ", fileDownloadFailures.length);
foreach(failedFileToDownload; fileDownloadFailures) {
// List the detail of the item that failed to download
log.logAndNotify("Failed to download: ", failedFileToDownload);
// Is this failed item in the DB? It should not be ..
Item downloadDBItem;
// Need to check all driveid's we know about, not just the defaultDriveId
foreach (searchDriveId; driveIDsArray) {
if (itemDB.selectByPath(failedFileToDownload, searchDriveId, downloadDBItem)) {
// item was found in the DB
log.error("ERROR: Failed Download Path found in database, must delete this item from the database .. it should not be in there if it failed to download");
// Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy
itemDB.deleteById(downloadDBItem.driveId, downloadDBItem.id);
if (downloadDBItem.remoteDriveId != null) {
// delete the linked remote folder
itemDB.deleteById(downloadDBItem.remoteDriveId, downloadDBItem.remoteId);
}
}
}
}
// Set the flag
syncFailures = true;
}
// Were there any file upload failures?
if (!fileUploadFailures.empty) {
// There are download failures ...
log.log("\nFailed items to upload to OneDrive: ", fileUploadFailures.length);
foreach(failedFileToUpload; fileUploadFailures) {
// List the path of the item that failed to upload
log.logAndNotify("Failed to upload: ", failedFileToUpload);
// Is this failed item in the DB? It should not be ..
Item uploadDBItem;
// Need to check all driveid's we know about, not just the defaultDriveId
foreach (searchDriveId; driveIDsArray) {
if (itemDB.selectByPath(failedFileToUpload, searchDriveId, uploadDBItem)) {
// item was found in the DB
log.error("ERROR: Failed Upload Path found in database, must delete this item from the database .. it should not be in there if it failed to upload");
// Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy
itemDB.deleteById(uploadDBItem.driveId, uploadDBItem.id);
if (uploadDBItem.remoteDriveId != null) {
// delete the linked remote folder
itemDB.deleteById(uploadDBItem.remoteDriveId, uploadDBItem.remoteId);
}
}
}
}
// Set the flag
syncFailures = true;
}
}
// Generate a /delta compatible response - for use when we cant actually use /delta
// This is required when the application is configured to use National Azure AD deployments as these do not support /delta queries
// The same technique can also be used when we are using --single-directory. The parent objects up to the single directory target can be added,
// then once the target of the --single-directory request is hit, all of the children of that path can be queried, giving a much more focused
// JSON response which can then be processed, negating the need to continuously traverse the tree and 'exclude' items
JSONValue generateDeltaResponse(string pathToQuery = null) {
// JSON value which will be responded with
JSONValue selfGeneratedDeltaResponse;
// Function variables
Item searchItem;
JSONValue rootData;
JSONValue driveData;
JSONValue pathData;
JSONValue topLevelChildren;
JSONValue[] childrenData;
string nextLink;
// Was a path to query passed in?
if (pathToQuery.empty) {
// Will query for the 'root'
pathToQuery = ".";
}
// Create new OneDrive API Instance
OneDriveApi generateDeltaResponseOneDriveApiInstance;
generateDeltaResponseOneDriveApiInstance = new OneDriveApi(appConfig);
generateDeltaResponseOneDriveApiInstance.initialise();
if (!singleDirectoryScope) {
// In a --resync scenario, there is no DB data to query, so we have to query the OneDrive API here to get relevant details
try {
// Query the OneDrive API
pathData = generateDeltaResponseOneDriveApiInstance.getPathDetails(pathToQuery);
// Is the path on OneDrive local or remote to our account drive id?
if (isItemRemote(pathData)) {
// The path we are seeking is remote to our account drive id
searchItem.driveId = pathData["remoteItem"]["parentReference"]["driveId"].str;
searchItem.id = pathData["remoteItem"]["id"].str;
} else {
// The path we are seeking is local to our account drive id
searchItem.driveId = pathData["parentReference"]["driveId"].str;
searchItem.id = pathData["id"].str;
}
} catch (OneDriveException e) {
// Display error message
displayOneDriveErrorMessage(e.msg, getFunctionName!({}));
// Must exit here
generateDeltaResponseOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(generateDeltaResponseOneDriveApiInstance);
exit(-1);
}
} else {
// When setSingleDirectoryScope() was called, the following were set to the correct items, even if the path was remote:
// - singleDirectoryScopeDriveId
// - singleDirectoryScopeItemId
// Reuse these prior set values
searchItem.driveId = singleDirectoryScopeDriveId;
searchItem.id = singleDirectoryScopeItemId;
}
// Before we get any data from the OneDrive API, flag any child object in the database as out-of-sync for this driveId & and object id
// Downgrade ONLY files associated with this driveId and idToQuery
log.vdebug("Downgrading all children for this searchItem.driveId (" ~ searchItem.driveId ~ ") and searchItem.id (" ~ searchItem.id ~ ") to an out-of-sync state");
auto drivePathChildren = getChildren(searchItem.driveId, searchItem.id);
if (count(drivePathChildren) > 0) {
// Children to process and flag as out-of-sync
foreach (drivePathChild; drivePathChildren) {
// Flag any object in the database as out-of-sync for this driveId & and object id
log.vdebug("Downgrading item as out-of-sync: ", drivePathChild.id);
itemDB.downgradeSyncStatusFlag(drivePathChild.driveId, drivePathChild.id);
}
}
// Get drive details for the provided driveId
try {
driveData = generateDeltaResponseOneDriveApiInstance.getPathDetailsById(searchItem.driveId, searchItem.id);
} catch (OneDriveException exception) {
log.vdebug("driveData = generateDeltaResponseOneDriveApiInstance.getPathDetailsById(searchItem.driveId, searchItem.id) generated a OneDriveException");
string thisFunctionName = getFunctionName!({});
// 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(generateDeltaResponseOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to query path details on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
generateDeltaResponse(pathToQuery);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
// Was a valid JSON response for 'driveData' provided?
if (driveData.type() == JSONType.object) {
// Dynamic output for a non-verbose run so that the user knows something is happening
if (log.verbose == 0) {
if (!appConfig.surpressLoggingOutput) {
log.fileOnly("Fetching items from the OneDrive API for Drive ID: ", searchItem.driveId);
// Use the dots to show the application is 'doing something'
write("Fetching items from the OneDrive API for Drive ID: ", searchItem.driveId, " .");
}
} else {
log.vlog("Generating a /delta response from the OneDrive API for Drive ID: ", searchItem.driveId);
}
// Process this initial JSON response
if (!isItemRoot(driveData)) {
// Get root details for the provided driveId
try {
rootData = generateDeltaResponseOneDriveApiInstance.getDriveIdRoot(searchItem.driveId);
} catch (OneDriveException exception) {
log.vdebug("rootData = onedrive.getDriveIdRoot(searchItem.driveId) generated a OneDriveException");
string thisFunctionName = getFunctionName!({});
// 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(generateDeltaResponseOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to query drive root details on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.log("Retrying Query: rootData = generateDeltaResponseOneDriveApiInstance.getDriveIdRoot(searchItem.driveId)");
rootData = generateDeltaResponseOneDriveApiInstance.getDriveIdRoot(searchItem.driveId);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
// Add driveData JSON data to array
log.vlog("Adding OneDrive root details for processing");
childrenData ~= rootData;
}
// Add driveData JSON data to array
log.vlog("Adding OneDrive folder details for processing");
childrenData ~= driveData;
} else {
// driveData is an invalid JSON object
writeln("CODING TO DO: The query of OneDrive API to getPathDetailsById generated an invalid JSON response - thus we cant build our own /delta simulated response ... how to handle?");
// Must exit here
generateDeltaResponseOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(generateDeltaResponseOneDriveApiInstance);
exit(-1);
}
// For each child object, query the OneDrive API
for (;;) {
// query top level children
try {
topLevelChildren = generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink);
} catch (OneDriveException exception) {
// OneDrive threw an error
log.vdebug("------------------------------------------------------------------");
log.vdebug("Query Error: topLevelChildren = generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink)");
log.vdebug("driveId: ", searchItem.driveId);
log.vdebug("idToQuery: ", searchItem.id);
log.vdebug("nextLink: ", nextLink);
string thisFunctionName = getFunctionName!({});
// 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(generateDeltaResponseOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry topLevelChildren = generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink)");
}
// 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);
log.log(errorArray[0], " when attempting to query OneDrive top level drive children on OneDrive - retrying applicable request in 30 seconds");
log.vdebug("generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink) previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
//log.vdebug("Retrying Query: generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink)");
//topLevelChildren = generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink);
log.vdebug("Retrying Function: ", thisFunctionName);
generateDeltaResponse(pathToQuery);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
// process top level children
log.vlog("Adding ", count(topLevelChildren["value"].array), " OneDrive items for processing from the OneDrive 'root' folder");
foreach (child; topLevelChildren["value"].array) {
// Check for any Client Side Filtering here ... we should skip querying the OneDrive API for 'folders' that we are going to just process and skip anyway.
// This avoids needless calls to the OneDrive API, and potentially speeds up this process.
if (!checkJSONAgainstClientSideFiltering(child)) {
// add this child to the array of objects
childrenData ~= child;
// is this child a folder?
if (isItemFolder(child)) {
// We have to query this folders children if childCount > 0
if (child["folder"]["childCount"].integer > 0){
// This child folder has children
string childIdToQuery = child["id"].str;
string childDriveToQuery = child["parentReference"]["driveId"].str;
auto childParentPath = child["parentReference"]["path"].str.split(":");
string folderPathToScan = childParentPath[1] ~ "/" ~ child["name"].str;
string pathForLogging;
// Are we in a --single-directory situation? If we are, the path we are using for logging needs to use the input path as a base
if (singleDirectoryScope) {
pathForLogging = appConfig.getValueString("single_directory") ~ "/" ~ child["name"].str;
} else {
pathForLogging = child["name"].str;
}
// Query the children of this item
JSONValue[] grandChildrenData = queryForChildren(childDriveToQuery, childIdToQuery, folderPathToScan, pathForLogging);
foreach (grandChild; grandChildrenData.array) {
// add the grandchild to the array
childrenData ~= grandChild;
}
}
}
}
}
// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response
// to indicate more items are available and provide the request URL for the next page of items.
if ("@odata.nextLink" in topLevelChildren) {
// Update nextLink to next changeSet bundle
log.vdebug("Setting nextLink to (@odata.nextLink): ", nextLink);
nextLink = topLevelChildren["@odata.nextLink"].str;
} else break;
}
if (log.verbose == 0) {
// Dynamic output for a non-verbose run so that the user knows something is happening
if (!appConfig.surpressLoggingOutput) {
writeln();
}
}
// Craft response from all returned JSON elements
selfGeneratedDeltaResponse = [
"@odata.context": JSONValue("https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)"),
"value": JSONValue(childrenData.array)
];
// Shutdown API
generateDeltaResponseOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(generateDeltaResponseOneDriveApiInstance);
// Return the generated JSON response
return selfGeneratedDeltaResponse;
}
// Query the OneDrive API for the specified child id for any children objects
JSONValue[] queryForChildren(string driveId, string idToQuery, string childParentPath, string pathForLogging) {
// function variables
JSONValue thisLevelChildren;
JSONValue[] thisLevelChildrenData;
string nextLink;
// Create new OneDrive API Instance
OneDriveApi queryChildrenOneDriveApiInstance;
queryChildrenOneDriveApiInstance = new OneDriveApi(appConfig);
queryChildrenOneDriveApiInstance.initialise();
for (;;) {
// query this level children
try {
thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance);
} catch (OneDriveException exception) {
writeln("CODING TO DO: EXCEPTION HANDLING NEEDED: thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance)");
}
if (log.verbose == 0) {
// Dynamic output for a non-verbose run so that the user knows something is happening
if (!appConfig.surpressLoggingOutput) {
write(".");
}
}
// Was a valid JSON response for 'thisLevelChildren' provided?
if (thisLevelChildren.type() == JSONType.object) {
// process this level children
if (!childParentPath.empty) {
// We dont use childParentPath to log, as this poses an information leak risk.
// The full parent path of the child, as per the JSON might be:
// /Level 1/Level 2/Level 3/Child Shared Folder/some folder/another folder
// But 'Child Shared Folder' is what is shared, thus '/Level 1/Level 2/Level 3/' is a potential information leak if logged.
// Plus, the application output now shows accuratly what is being shared - so that is a good thing.
log.vlog("Adding ", count(thisLevelChildren["value"].array), " OneDrive items for processing from ", pathForLogging);
}
foreach (child; thisLevelChildren["value"].array) {
// Check for any Client Side Filtering here ... we should skip querying the OneDrive API for 'folders' that we are going to just process and skip anyway.
// This avoids needless calls to the OneDrive API, and potentially speeds up this process.
if (!checkJSONAgainstClientSideFiltering(child)) {
// add this child to the array of objects
thisLevelChildrenData ~= child;
// is this child a folder?
if (isItemFolder(child)){
// We have to query this folders children if childCount > 0
if (child["folder"]["childCount"].integer > 0){
// This child folder has children
string childIdToQuery = child["id"].str;
string childDriveToQuery = child["parentReference"]["driveId"].str;
auto grandchildParentPath = child["parentReference"]["path"].str.split(":");
string folderPathToScan = grandchildParentPath[1] ~ "/" ~ child["name"].str;
string newLoggingPath = pathForLogging ~ "/" ~ child["name"].str;
JSONValue[] grandChildrenData = queryForChildren(childDriveToQuery, childIdToQuery, folderPathToScan, newLoggingPath);
foreach (grandChild; grandChildrenData.array) {
// add the grandchild to the array
thisLevelChildrenData ~= grandChild;
}
}
}
}
}
// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response
// to indicate more items are available and provide the request URL for the next page of items.
if ("@odata.nextLink" in thisLevelChildren) {
// Update nextLink to next changeSet bundle
nextLink = thisLevelChildren["@odata.nextLink"].str;
log.vdebug("Setting nextLink to (@odata.nextLink): ", nextLink);
} else break;
} else {
// Invalid JSON response when querying this level children
log.vdebug("INVALID JSON response when attempting a retry of parent function - queryForChildren(driveId, idToQuery, childParentPath, pathForLogging)");
// retry thisLevelChildren = queryThisLevelChildren
log.vdebug("Thread sleeping for an additional 30 seconds");
Thread.sleep(dur!"seconds"(30));
log.vdebug("Retry this call thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance)");
thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance);
}
}
// Shutdown API instance
queryChildrenOneDriveApiInstance.shutdown();
// Free object and memory
object.destroy(queryChildrenOneDriveApiInstance);
// return response
return thisLevelChildrenData;
}
// Query the OneDrive API for the child objects for this element
JSONValue queryThisLevelChildren(string driveId, string idToQuery, string nextLink, OneDriveApi queryChildrenOneDriveApiInstance) {
// function variables
JSONValue thisLevelChildren;
// query children
try {
// attempt API call
log.vdebug("Attempting Query: thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)");
thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink);
log.vdebug("Query 'thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)' performed successfully");
} catch (OneDriveException exception) {
// OneDrive threw an error
log.vdebug("------------------------------------------------------------------");
log.vdebug("Query Error: thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)");
log.vdebug("driveId: ", driveId);
log.vdebug("idToQuery: ", idToQuery);
log.vdebug("nextLink: ", nextLink);
string thisFunctionName = getFunctionName!({});
// 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(queryChildrenOneDriveApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to query OneDrive drive item children - retrying applicable request in 30 seconds");
log.vdebug("thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink) previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
// return response
return thisLevelChildren;
}
// Traverses the provided path online, via the OneDrive API, following correct parent driveId and itemId elements across the account
// to find if this full path exists. If this path exists online, the last item in the object path will be returned as a full JSON item.
//
// If the createPathIfMissing = false + no path exists online, a null invalid JSON item will be returned.
// If the createPathIfMissing = true + no path exists online, the requested path will be created in the correct location online. The resulting
// response to the directory creation will then be returned.
//
// This function also ensures that each path in the requested path actually matches the requested element to ensure that the OneDrive API response
// is not falsely matching a 'case insensitive' match to the actual request which is a POSIX compliance issue.
JSONValue queryOneDriveForSpecificPathAndCreateIfMissing(string thisNewPathToSearch, bool createPathIfMissing) {
// function variables
JSONValue getPathDetailsAPIResponse;
string currentPathTree;
Item parentDetails;
JSONValue topLevelChildren;
string nextLink;
bool directoryFoundOnline = false;
bool posixIssue = false;
// Create a new API Instance for this thread and initialise it
OneDriveApi queryOneDriveForSpecificPath;
queryOneDriveForSpecificPath = new OneDriveApi(appConfig);
queryOneDriveForSpecificPath.initialise();
foreach (thisFolderName; pathSplitter(thisNewPathToSearch)) {
log.vdebug("Testing for the existance online of this folder path: ", thisFolderName);
directoryFoundOnline = false;
// If this is '.' this is the account root
if (thisFolderName == ".") {
currentPathTree = thisFolderName;
} else {
currentPathTree = currentPathTree ~ "/" ~ thisFolderName;
}
log.vdebug("Attempting to query OneDrive for this path: ", currentPathTree);
// What query do we use?
if (thisFolderName == ".") {
// Query the root, set the right details
try {
getPathDetailsAPIResponse = queryOneDriveForSpecificPath.getPathDetails(currentPathTree);
parentDetails = makeItem(getPathDetailsAPIResponse);
// Save item to the database
saveItem(getPathDetailsAPIResponse);
directoryFoundOnline = true;
} catch (OneDriveException exception) {
string thisFunctionName = getFunctionName!({});
// 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(queryOneDriveForSpecificPath);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to query path on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
queryOneDriveForSpecificPathAndCreateIfMissing(thisNewPathToSearch, createPathIfMissing);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
} else {
// Ensure we have a valid driveId to search here
if (parentDetails.driveId.empty) {
parentDetails.driveId = appConfig.defaultDriveId;
}
// If the prior JSON 'getPathDetailsAPIResponse' is on this account driveId .. then continue to use getPathDetails
if (parentDetails.driveId == appConfig.defaultDriveId) {
try {
// Query OneDrive API for this path
getPathDetailsAPIResponse = queryOneDriveForSpecificPath.getPathDetails(currentPathTree);
// Portable Operating System Interface (POSIX) testing of JSON response from OneDrive API
performPosixTest(thisFolderName, getPathDetailsAPIResponse["name"].str);
// No POSIX issue with requested path element
parentDetails = makeItem(getPathDetailsAPIResponse);
// Save item to the database
saveItem(getPathDetailsAPIResponse);
directoryFoundOnline = true;
// Is this JSON a remote object
if (isItemRemote(getPathDetailsAPIResponse)) {
// Remote Directory .. need a DB Tie Item
log.vdebug("Creating a DB TIE for this Shared Folder");
// New DB Tie Item to bind the 'remote' path to our parent path
Item tieDBItem;
// Set the name
tieDBItem.name = parentDetails.name;
// Set the correct item type
tieDBItem.type = ItemType.dir;
// Set the right elements using the 'remote' of the parent as the 'actual' for this DB Tie
tieDBItem.driveId = parentDetails.remoteDriveId;
tieDBItem.id = parentDetails.remoteId;
// Set the correct mtime
tieDBItem.mtime = parentDetails.mtime;
// Add tie DB record to the local database
log.vdebug("Adding tie DB record to database: ", tieDBItem);
itemDB.upsert(tieDBItem);
// Update parentDetails to use the DB Tie record
parentDetails = tieDBItem;
}
} catch (OneDriveException exception) {
if (exception.httpStatusCode == 404) {
directoryFoundOnline = false;
} else {
string thisFunctionName = getFunctionName!({});
// 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(queryOneDriveForSpecificPath);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to retry ", thisFunctionName);
}
// 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);
log.log(errorArray[0], " when attempting to query path on OneDrive - retrying applicable request in 30 seconds");
log.vdebug(thisFunctionName, " previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429, 503, 504 - but loop back calling this function
log.vdebug("Retrying Function: ", thisFunctionName);
queryOneDriveForSpecificPathAndCreateIfMissing(thisNewPathToSearch, createPathIfMissing);
} else {
// Default operation if not 408,429,503,504 errors
// display what the error is
displayOneDriveErrorMessage(exception.msg, thisFunctionName);
}
}
}
} else {
// parentDetails.driveId is not the account drive id - thus will be a remote shared item
log.vdebug("This parent directory is a remote object this next path will be on a remote drive");
// For this parentDetails.driveId, parentDetails.id object, query the OneDrive API for it's children
for (;;) {
// Query this remote object for its children
topLevelChildren = queryOneDriveForSpecificPath.listChildren(parentDetails.driveId, parentDetails.id, nextLink);
// Process each child
foreach (child; topLevelChildren["value"].array) {
// Is this child a folder?
if (isItemFolder(child)) {
// Is this the child folder we are looking for, and is a POSIX match?
if (child["name"].str == thisFolderName) {
// EXACT MATCH including case sensitivity: Flag that we found the folder online
directoryFoundOnline = true;
// Use these details for the next entry path
getPathDetailsAPIResponse = child;
parentDetails = makeItem(getPathDetailsAPIResponse);
// Save item to the database
saveItem(getPathDetailsAPIResponse);
// No need to continue searching
break;
} else {
string childAsLower = toLower(child["name"].str);
string thisFolderNameAsLower = toLower(thisFolderName);
if (childAsLower == thisFolderNameAsLower) {
// This is a POSIX 'case in-sensitive match' .....
// Local item name has a 'case-insensitive match' to an existing item on OneDrive
posixIssue = true;
throw new posixException(thisFolderName, child["name"].str);
}
}
}
}
if (directoryFoundOnline) {
// We found the folder, no need to continue searching nextLink data
break;
}
// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response
// to indicate more items are available and provide the request URL for the next page of items.
if ("@odata.nextLink" in topLevelChildren) {
// Update nextLink to next changeSet bundle
log.vdebug("Setting nextLink to (@odata.nextLink): ", nextLink);
nextLink = topLevelChildren["@odata.nextLink"].str;
} else break;
}
}
}
// If we did not find the folder, we need to create this folder
if (!directoryFoundOnline) {
// Folder not found online
// Set any response to be an invalid JSON item
getPathDetailsAPIResponse = null;
// Was there a POSIX issue?
if (!posixIssue) {
// No POSIX issue
if (createPathIfMissing) {
// Create this path as it is missing on OneDrive online and there is no POSIX issue with a 'case-insensitive match'
log.vdebug("FOLDER NOT FOUND ONLINE AND WE ARE REQUESTED TO CREATE IT");
log.vdebug("Create folder on this drive: ", parentDetails.driveId);
log.vdebug("Create folder as a child on this object: ", parentDetails.id);
log.vdebug("Create this folder name: ", thisFolderName);
JSONValue newDriveItem = [
"name": JSONValue(thisFolderName),
"folder": parseJSON("{}")
];
JSONValue createByIdAPIResponse;
// Submit the creation request
// Fix for https://github.com/skilion/onedrive/issues/356
if (!dryRun) {
try {
// Attempt to create a new folder on the configured parent driveId & parent id
createByIdAPIResponse = queryOneDriveForSpecificPath.createById(parentDetails.driveId, parentDetails.id, newDriveItem);
// Is the response a valid JSON object - validation checking done in saveItem
saveItem(createByIdAPIResponse);
// Set getPathDetailsAPIResponse to createByIdAPIResponse
getPathDetailsAPIResponse = createByIdAPIResponse;
} catch (OneDriveException e) {
// 409 - API Race Condition
if (e.httpStatusCode == 409) {
// When we attempted to create it, OneDrive responded that it now already exists
log.vlog("OneDrive reported that ", thisFolderName, " already exists .. OneDrive API race condition");
} else {
// some other error from OneDrive was returned - display what it is
log.error("OneDrive generated an error when creating this path: ", thisFolderName);
displayOneDriveErrorMessage(e.msg, getFunctionName!({}));
}
}
} else {
// Simulate a successful 'directory create' & save it to the dryRun database copy
// The simulated response has to pass 'makeItem' as part of saveItem
auto fakeResponse = createFakeResponse(thisNewPathToSearch);
// Save item to the database
saveItem(fakeResponse);
}
}
}
}
}
// Shutdown API instance
queryOneDriveForSpecificPath.shutdown();
// Free object and memory
object.destroy(queryOneDriveForSpecificPath);
// Output our search results
log.vdebug("queryOneDriveForSpecificPathAndCreateIfMissing.getPathDetailsAPIResponse = ", getPathDetailsAPIResponse);
return getPathDetailsAPIResponse;
}
// Delete an item by it's path
// This function is only used in --monitor mode and --remove-directory directive
void deleteByPath(string path) {
// function variables
Item dbItem;
// Need to check all driveid's we know about, not just the defaultDriveId
bool itemInDB = false;
foreach (searchDriveId; driveIDsArray) {
if (itemDB.selectByPath(path, searchDriveId, dbItem)) {
// item was found in the DB
itemInDB = true;
break;
}
}
// Was the item found in the database?
if (!itemInDB) {
// path to delete is not in the local database ..
// was this a --remove-directory attempt?
if (!appConfig.getValueBool("monitor")) {
// --remove-directory deletion attempt
log.error("The item to delete is not in the local database - unable to delete online");
return;
} else {
// normal use .. --monitor being used
throw new SyncException("The item to delete is not in the local database");
}
}
// This needs to be enforced as we have to know the parent id of the object being deleted
if (dbItem.parentId == null) {
// the item is a remote folder, need to do the operation on the parent
enforce(itemDB.selectByPathWithoutRemote(path, appConfig.defaultDriveId, dbItem));
}
try {
if (noRemoteDelete) {
// do not process remote delete
log.vlog("Skipping remote delete as --upload-only & --no-remote-delete configured");
} else {
uploadDeletedItem(dbItem, path);
}
} catch (OneDriveException e) {
if (e.httpStatusCode == 404) {
log.log(e.msg);
} else {
// display what the error is
displayOneDriveErrorMessage(e.msg, getFunctionName!({}));
}
}
}
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_move
// This function is only called in monitor mode when an move event is coming from
// inotify and we try to move the item.
void uploadMoveItem(string oldPath, string newPath) {
// Log that we are doing a move
log.log("Moving ", oldPath, " to ", newPath);
// Is this move unwanted?
bool unwanted = false;
// Item variables
Item oldItem, newItem, parentItem;
// This not a Client Side Filtering check, nor a Microsoft Check, but is a sanity check that the path provided is UTF encoded correctly
// Check the std.encoding of the path against: Unicode 5.0, ASCII, ISO-8859-1, ISO-8859-2, WINDOWS-1250, WINDOWS-1251, WINDOWS-1252
if (!unwanted) {
if(!isValid(newPath)) {
// Path is not valid according to https://dlang.org/phobos/std_encoding.html
log.logAndNotify("Skipping item - invalid character encoding sequence: ", newPath);
unwanted = true;
}
}
// Check this path against the Client Side Filtering Rules
// - check_nosync
// - skip_dotfiles
// - skip_symlinks
// - skip_file
// - skip_dir
// - sync_list
// - skip_size
if (!unwanted) {
unwanted = checkPathAgainstClientSideFiltering(newPath);
}
// Check against Microsoft OneDrive restriction and limitations about Windows naming files
if (!unwanted) {
unwanted = checkPathAgainstMicrosoftNamingRestrictions(newPath);
}
// 'newPath' has passed client side filtering validation
if (!unwanted) {
if (!itemDB.selectByPath(oldPath, appConfig.defaultDriveId, oldItem)) {
// The old path|item is not synced with the database, upload as a new file
log.log("Moved local item was not in-sync with local databse - uploading as new item");
uploadNewFile(newPath);
return;
}
if (oldItem.parentId == null) {
// the item is a remote folder, need to do the operation on the parent
enforce(itemDB.selectByPathWithoutRemote(oldPath, appConfig.defaultDriveId, oldItem));
}
if (itemDB.selectByPath(newPath, appConfig.defaultDriveId, newItem)) {
// the destination has been overwritten
log.log("Moved local item overwrote an existing item - deleting old online item");
uploadDeletedItem(newItem, newPath);
}
if (!itemDB.selectByPath(dirName(newPath), appConfig.defaultDriveId, parentItem)) {
// the parent item is not in the database
throw new SyncException("Can't move an item to an unsynced directory");
}
if (oldItem.driveId != parentItem.driveId) {
// items cannot be moved between drives
uploadDeletedItem(oldItem, oldPath);
// what sort of move is this?
if (isFile(newPath)) {
// newPath is a file
uploadNewFile(newPath);
} else {
// newPath is a directory
scanLocalFilesystemPathForNewData(newPath);
}
} else {
if (!exists(newPath)) {
// is this --monitor use?
if (appConfig.getValueBool("monitor")) {
log.vlog("uploadMoveItem target has disappeared: ", newPath);
return;
}
}
// Configure the modification JSON item
SysTime mtime;
if (appConfig.getValueBool("monitor")) {
// Use the newPath modified timestamp
mtime = timeLastModified(newPath).toUTC();
} else {
// Use the current system time
mtime = Clock.currTime().toUTC();
}
JSONValue data = [
"name": JSONValue(baseName(newPath)),
"parentReference": JSONValue([
"id": parentItem.id
]),
"fileSystemInfo": JSONValue([
"lastModifiedDateTime": mtime.toISOExtString()
])
];
// Perform the move operation on OneDrive
JSONValue response;
// Create a new API Instance for this thread and initialise it
OneDriveApi movePathOnlineApiInstance;
movePathOnlineApiInstance = new OneDriveApi(appConfig);
movePathOnlineApiInstance.initialise();
try {
response = movePathOnlineApiInstance.updateById(oldItem.driveId, oldItem.id, data, oldItem.eTag);
} catch (OneDriveException e) {
if (e.httpStatusCode == 412) {
// OneDrive threw a 412 error, most likely: ETag does not match current item's value
// Retry without eTag
log.vdebug("File Move Failed - OneDrive eTag / cTag match issue");
log.vlog("OneDrive returned a 'HTTP 412 - Precondition Failed' when attempting to move the file - gracefully handling error");
string nullTag = null;
// move the file but without the eTag
response = movePathOnlineApiInstance.updateById(oldItem.driveId, oldItem.id, data, nullTag);
}
}
// Shutdown API instance
movePathOnlineApiInstance.shutdown();
// Free object and memory
object.destroy(movePathOnlineApiInstance);
// save the move response from OneDrive in the database
// Is the response a valid JSON object - validation checking done in saveItem
saveItem(response);
}
} else {
// Moved item is unwanted
log.log("Item has been moved to a location that is excluded from sync operations. Removing item from OneDrive");
uploadDeletedItem(oldItem, oldPath);
}
}
// Perform integrity validation of the file that was uploaded
bool performUploadIntegrityValidationChecks(JSONValue uploadResponse, string localFilePath, ulong localFileSize) {
bool integrityValid = false;
if (!disableUploadValidation) {
// Integrity validation has not been disabled (this is the default so we are always integrity checking our uploads)
if (uploadResponse.type() == JSONType.object) {
// Provided JSON is a valid JSON
ulong uploadFileSize = uploadResponse["size"].integer;
string uploadFileHash = uploadResponse["file"]["hashes"]["quickXorHash"].str;
string localFileHash = computeQuickXorHash(localFilePath);
if ((localFileSize == uploadFileSize) && (localFileHash == uploadFileHash)) {
// Uploaded file integrity intact
log.vdebug("Uploaded local file matches reported online size and hash values");
integrityValid = true;
} else {
// Upload integrity failure .. what failed?
// There are 2 scenarios where this happens:
// 1. Failed Transfer
// 2. Upload file is going to a SharePoint Site, where Microsoft enriches the file with additional metadata with no way to disable
log.logAndNotify("WARNING: Uploaded file integrity failure for: ", localFilePath);
// What integrity failed - size?
if (localFileSize != uploadFileSize) {
log.vlog("WARNING: Uploaded file integrity failure - Size Mismatch");
}
// What integrity failed - hash?
if (localFileHash != uploadFileHash) {
log.vlog("WARNING: Uploaded file integrity failure - Hash Mismatch");
}
// What account type is this?
if (appConfig.accountType != "personal") {
// Not a personal account, thus the integrity failure is most likely due to SharePoint
log.vlog("CAUTION: Microsoft SharePoint enhances files after you upload them, which means this file may now have technical differences from your local copy, resulting in an integrity issue.");
log.vlog("See: https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details");
}
// How can this be disabled?
log.log("To disable the integrity checking of uploaded files use --disable-upload-validation");
}
} else {
log.log("Upload file validation unable to be performed: input JSON was invalid");
log.log("WARNING: Skipping upload integrity check for: ", localFilePath);
}
} else {
// We are bypassing integrity checks due to --disable-upload-validation
log.vdebug("Upload file validation disabled due to --disable-upload-validation");
log.vlog("WARNING: Skipping upload integrity check for: ", localFilePath);
}
// Is the file integrity online valid?
return integrityValid;
}
// Query Office 365 SharePoint Shared Library site name to obtain it's Drive ID
void querySiteCollectionForDriveID(string sharepointLibraryNameToQuery) {
// Steps to get the ID:
// 1. Query https://graph.microsoft.com/v1.0/sites?search= with the name entered
// 2. Evaluate the response. A valid response will contain the description and the id. If the response comes back with nothing, the site name cannot be found or no access
// 3. If valid, use the returned ID and query the site drives
// https://graph.microsoft.com/v1.0/sites/<site_id>/drives
// 4. Display Shared Library Name & Drive ID
string site_id;
string drive_id;
bool found = false;
JSONValue siteQuery;
string nextLink;
string[] siteSearchResults;
// Create a new API Instance for this thread and initialise it
OneDriveApi querySharePointLibraryNameApiInstance;
querySharePointLibraryNameApiInstance = new OneDriveApi(appConfig);
querySharePointLibraryNameApiInstance.initialise();
// The account type must not be a personal account type
if (appConfig.accountType == "personal") {
log.error("ERROR: A OneDrive Personal Account cannot be used with --get-sharepoint-drive-id. Please re-authenticate your client using a OneDrive Business Account.");
return;
}
// What query are we performing?
writeln();
log.log("Office 365 Library Name Query: ", sharepointLibraryNameToQuery);
for (;;) {
try {
siteQuery = querySharePointLibraryNameApiInstance.o365SiteSearch(nextLink);
} catch (OneDriveException e) {
log.error("ERROR: Query of OneDrive for Office 365 Library Name failed");
// Forbidden - most likely authentication scope needs to be updated
if (e.httpStatusCode == 403) {
log.error("ERROR: Authentication scope needs to be updated. Use --reauth and re-authenticate client.");
return;
}
// Requested resource cannot be found
if (e.httpStatusCode == 404) {
string siteSearchUrl;
if (nextLink.empty) {
siteSearchUrl = querySharePointLibraryNameApiInstance.getSiteSearchUrl();
} else {
siteSearchUrl = nextLink;
}
// log the error
log.error("ERROR: Your OneDrive Account and Authentication Scope cannot access this OneDrive API: ", siteSearchUrl);
log.error("ERROR: To resolve, please discuss this issue with whomever supports your OneDrive and SharePoint environment.");
return;
}
// HTTP request returned status code 429 (Too Many Requests)
if (e.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(querySharePointLibraryNameApiInstance);
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to query OneDrive drive children");
}
// HTTP request returned status code 504 (Gateway Timeout) or 429 retry
if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) {
// re-try the specific changes queries
if (e.httpStatusCode == 504) {
log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query Sharepoint Sites - retrying applicable request");
log.vdebug("siteQuery = onedrive.o365SiteSearch(nextLink) previously threw an error - retrying");
// 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.
log.vdebug("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");
Thread.sleep(dur!"seconds"(30));
}
// re-try original request - retried for 429 and 504
try {
log.vdebug("Retrying Query: siteQuery = onedrive.o365SiteSearch(nextLink)");
siteQuery = querySharePointLibraryNameApiInstance.o365SiteSearch(nextLink);
log.vdebug("Query 'siteQuery = onedrive.o365SiteSearch(nextLink)' performed successfully on re-try");
} catch (OneDriveException e) {
// display what the error is
log.vdebug("Query Error: siteQuery = onedrive.o365SiteSearch(nextLink) on re-try after delay");
// error was not a 504 this time
displayOneDriveErrorMessage(e.msg, getFunctionName!({}));
return;
}
} else {
// display what the error is
displayOneDriveErrorMessage(e.msg, getFunctionName!({}));
return;
}
}
// is siteQuery a valid JSON object & contain data we can use?
if ((siteQuery.type() == JSONType.object) && ("value" in siteQuery)) {
// valid JSON object
log.vdebug("O365 Query Response: ", siteQuery);
foreach (searchResult; siteQuery["value"].array) {
// Need an 'exclusive' match here with sharepointLibraryNameToQuery as entered
log.vdebug("Found O365 Site: ", searchResult);
// 'displayName' and 'id' have to be present in the search result record in order to query the site
if (("displayName" in searchResult) && ("id" in searchResult)) {
if (sharepointLibraryNameToQuery == searchResult["displayName"].str){
// 'displayName' matches search request
site_id = searchResult["id"].str;
JSONValue siteDriveQuery;
try {
siteDriveQuery = querySharePointLibraryNameApiInstance.o365SiteDrives(site_id);
} catch (OneDriveException e) {
log.error("ERROR: Query of OneDrive for Office Site ID failed");
// display what the error is
displayOneDriveErrorMessage(e.msg, getFunctionName!({}));
return;
}
// is siteDriveQuery a valid JSON object & contain data we can use?
if ((siteDriveQuery.type() == JSONType.object) && ("value" in siteDriveQuery)) {
// valid JSON object
foreach (driveResult; siteDriveQuery["value"].array) {
// Display results
writeln("-----------------------------------------------");
log.vdebug("Site Details: ", driveResult);
found = true;
writeln("Site Name: ", searchResult["displayName"].str);
writeln("Library Name: ", driveResult["name"].str);
writeln("drive_id: ", driveResult["id"].str);
writeln("Library URL: ", driveResult["webUrl"].str);
}
// closeout
writeln("-----------------------------------------------");
} else {
// not a valid JSON object
log.error("ERROR: There was an error performing this operation on OneDrive");
log.error("ERROR: Increase logging verbosity to assist determining why.");
return;
}
}
} else {
// 'displayName', 'id' or ''webUrl' not present in JSON results for a specific site
string siteNameAvailable = "Site 'name' was restricted by OneDrive API permissions";
bool displayNameAvailable = false;
bool idAvailable = false;
if ("name" in searchResult) siteNameAvailable = searchResult["name"].str;
if ("displayName" in searchResult) displayNameAvailable = true;
if ("id" in searchResult) idAvailable = true;
// Display error details for this site data
writeln();
log.error("ERROR: SharePoint Site details not provided for: ", siteNameAvailable);
log.error("ERROR: The SharePoint Site results returned from OneDrive API do not contain the required items to match. Please check your permissions with your site administrator.");
log.error("ERROR: Your site security settings is preventing the following details from being accessed: 'displayName' or 'id'");
log.vlog(" - Is 'displayName' available = ", displayNameAvailable);
log.vlog(" - Is 'id' available = ", idAvailable);
log.error("ERROR: To debug this further, please increase verbosity (--verbose or --verbose --verbose) to provide further insight as to what details are actually being returned.");
}
}
if(!found) {
// The SharePoint site we are searching for was not found in this bundle set
// Add to siteSearchResults so we can display what we did find
string siteSearchResultsEntry;
foreach (searchResult; siteQuery["value"].array) {
// We can only add the displayName if it is available
if ("displayName" in searchResult) {
// Use the displayName
siteSearchResultsEntry = " * " ~ searchResult["displayName"].str;
siteSearchResults ~= siteSearchResultsEntry;
} else {
// Add, but indicate displayName unavailable, use id
if ("id" in searchResult) {
siteSearchResultsEntry = " * " ~ "Unknown displayName (Data not provided by API), Site ID: " ~ searchResult["id"].str;
siteSearchResults ~= siteSearchResultsEntry;
} else {
// displayName and id unavailable, display in debug log the entry
log.vdebug("Bad SharePoint Data for site: ", searchResult);
}
}
}
}
} else {
// not a valid JSON object
log.error("ERROR: There was an error performing this operation on OneDrive");
log.error("ERROR: Increase logging verbosity to assist determining why.");
return;
}
// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response
// to indicate more items are available and provide the request URL for the next page of items.
if ("@odata.nextLink" in siteQuery) {
// Update nextLink to next set of SharePoint library names
nextLink = siteQuery["@odata.nextLink"].str;
log.vdebug("Setting nextLink to (@odata.nextLink): ", nextLink);
} else break;
}
// Was the intended target found?
if(!found) {
// Was the search a wildcard?
if (sharepointLibraryNameToQuery != "*") {
// Only print this out if the search was not a wildcard
writeln();
log.error("ERROR: The requested SharePoint site could not be found. Please check it's name and your permissions to access the site.");
}
// List all sites returned to assist user
writeln();
log.log("The following SharePoint site names were returned:");
foreach (searchResultEntry; siteSearchResults) {
// list the display name that we use to match against the user query
log.log(searchResultEntry);
}
}
// Shutdown API instance
querySharePointLibraryNameApiInstance.shutdown();
// Free object and memory
object.destroy(querySharePointLibraryNameApiInstance);
}
// Query the sync status of the client and the local system
void queryOneDriveForSyncStatus(string pathToQueryStatusOn) {
// Query the account driveId and rootId to get the /delta JSON information
// Process that JSON data for relevancy
// Function variables
ulong downloadSize = 0;
string deltaLink = null;
string driveIdToQuery = appConfig.defaultDriveId;
string itemIdToQuery = appConfig.defaultRootId;
JSONValue deltaChanges;
// Array of JSON items
JSONValue[] jsonItemsArray;
// Query Database for a potential deltaLink starting point
deltaLink = itemDB.getDeltaLink(driveIdToQuery, itemIdToQuery);
write("Querying the change status of Drive ID: ", driveIdToQuery, " .");
// Query the OenDrive API using the applicable details, following nextLink if applicable
// Create a new API Instance for querying /delta and initialise it
OneDriveApi getDeltaQueryOneDriveApiInstance;
getDeltaQueryOneDriveApiInstance = new OneDriveApi(appConfig);
getDeltaQueryOneDriveApiInstance.initialise();
for (;;) {
// Add a processing '.'
write(".");
// Get the /delta changes via the OneDrive API
// getDeltaChangesByItemId has the re-try logic for transient errors
deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, deltaLink, 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
log.vdebug("ERROR: Query of the OneDrive API via deltaChanges = getDeltaChangesByItemId() returned an invalid JSON response");
deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, deltaLink, getDeltaQueryOneDriveApiInstance);
}
}
// 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) {
// is the JSON a root object - we dont want to count this
if (!isItemRoot(onedriveJSONItem)) {
// Files are the only item that we want to calculate
if (isItemFile(onedriveJSONItem)) {
// JSON item is a file
// Is the item filtered out due to client side filtering rules?
if (!checkJSONAgainstClientSideFiltering(onedriveJSONItem)) {
// Is the path of this JSON item 'in-scope' or 'out-of-scope' ?
if (pathToQueryStatusOn != "/") {
// We need to check the path of this item against pathToQueryStatusOn
string thisItemPath = "";
if (("path" in onedriveJSONItem["parentReference"]) != null) {
// If there is a parent reference path, try and use it
string selfBuiltPath = onedriveJSONItem["parentReference"]["path"].str ~ "/" ~ onedriveJSONItem["name"].str;
auto splitPath = selfBuiltPath.split("root:");
thisItemPath = splitPath[1];
} else {
// no parent reference path available
thisItemPath = onedriveJSONItem["name"].str;
}
// can we find 'pathToQueryStatusOn' in 'thisItemPath' ?
if (canFind(thisItemPath, pathToQueryStatusOn)) {
// Add this to the array for processing
jsonItemsArray ~= onedriveJSONItem;
}
} else {
// We are not doing a --single-directory check
// Add this to the array for processing
jsonItemsArray ~= onedriveJSONItem;
}
}
}
}
}
// The response may contain either @odata.deltaLink or @odata.nextLink
if ("@odata.deltaLink" in deltaChanges) {
deltaLink = deltaChanges["@odata.deltaLink"].str;
log.vdebug("Setting next deltaLink to (@odata.deltaLink): ", deltaLink);
}
// Update deltaLink to next changeSet bundle
if ("@odata.nextLink" in deltaChanges) {
deltaLink = deltaChanges["@odata.nextLink"].str;
log.vdebug("Setting next deltaLink to (@odata.nextLink): ", deltaLink);
}
else break;
}
// Needed after printing out '....' when fetching changes from OneDrive API
writeln();
// Are there any JSON items to process?
if (count(jsonItemsArray) != 0) {
// There are items to process
foreach (onedriveJSONItem; jsonItemsArray.array) {
// variables we need
string thisItemParentDriveId;
string thisItemId;
string thisItemHash;
bool existingDBEntry = false;
// Is this file a remote item (on a shared folder) ?
if (isItemRemote(onedriveJSONItem)) {
// remote drive item
thisItemParentDriveId = onedriveJSONItem["remoteItem"]["parentReference"]["driveId"].str;
thisItemId = onedriveJSONItem["id"].str;
} else {
// standard drive item
thisItemParentDriveId = onedriveJSONItem["parentReference"]["driveId"].str;
thisItemId = onedriveJSONItem["id"].str;
}
// Get the file hash
thisItemHash = onedriveJSONItem["file"]["hashes"]["quickXorHash"].str;
// Check if the item has been seen before
Item existingDatabaseItem;
existingDBEntry = itemDB.selectById(thisItemParentDriveId, thisItemId, existingDatabaseItem);
if (existingDBEntry) {
// item exists in database .. do the database details match the JSON record?
if (existingDatabaseItem.quickXorHash != thisItemHash) {
// file hash is different, will trigger a download event
downloadSize = downloadSize + onedriveJSONItem["size"].integer;
}
} else {
// item does not exist in the database
// this item has already passed client side filtering rules (skip_dir, skip_file, sync_list)
downloadSize = downloadSize + onedriveJSONItem["size"].integer;
}
}
}
// Was anything detected that would constitute a download?
if (downloadSize > 0) {
// we have something to download
if (pathToQueryStatusOn != "/") {
writeln("The selected local directory via --single-directory is out of sync with Microsoft OneDrive");
} else {
writeln("The configured local 'sync_dir' directory is out of sync with Microsoft OneDrive");
}
writeln("Approximate data to download from Microsoft OneDrive: ", (downloadSize/1024), " KB");
} else {
// No changes were returned
writeln("There are no pending changes from Microsoft OneDrive; your local directory matches the data online.");
}
}
// Query OneDrive for file details of a given path, returning either the 'webURL' or 'lastModifiedBy' JSON facet
void queryOneDriveForFileDetails(string inputFilePath, string runtimePath, string outputType) {
// Calculate the full local file path
string fullLocalFilePath = buildNormalizedPath(buildPath(runtimePath, inputFilePath));
// Query if file is valid locally
if (exists(fullLocalFilePath)) {
// search drive_id list
string[] distinctDriveIds = itemDB.selectDistinctDriveIds();
bool pathInDB = false;
Item dbItem;
foreach (searchDriveId; distinctDriveIds) {
// Does this path exist in the database, use the 'inputFilePath'
if (itemDB.selectByPath(inputFilePath, searchDriveId, dbItem)) {
// item is in the database
pathInDB = true;
JSONValue fileDetailsFromOneDrive;
// Create a new API Instance for this thread and initialise it
OneDriveApi queryOneDriveForFileDetailsApiInstance;
queryOneDriveForFileDetailsApiInstance = new OneDriveApi(appConfig);
queryOneDriveForFileDetailsApiInstance.initialise();
try {
fileDetailsFromOneDrive = queryOneDriveForFileDetailsApiInstance.getPathDetailsById(dbItem.driveId, dbItem.id);
} catch (OneDriveException exception) {
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
return;
}
// Is the API response a valid JSON file?
if (fileDetailsFromOneDrive.type() == JSONType.object) {
// debug output of response
log.vdebug("API Response: ", fileDetailsFromOneDrive);
// What sort of response to we generate
// --get-file-link response
if (outputType == "URL") {
if ((fileDetailsFromOneDrive.type() == JSONType.object) && ("webUrl" in fileDetailsFromOneDrive)) {
// Valid JSON object
writeln();
writeln("WebURL: ", fileDetailsFromOneDrive["webUrl"].str);
}
}
// --modified-by response
if (outputType == "ModifiedBy") {
if ((fileDetailsFromOneDrive.type() == JSONType.object) && ("lastModifiedBy" in fileDetailsFromOneDrive)) {
// Valid JSON object
writeln();
writeln("Last modified: ", fileDetailsFromOneDrive["lastModifiedDateTime"].str);
writeln("Last modified by: ", fileDetailsFromOneDrive["lastModifiedBy"]["user"]["displayName"].str);
// if 'email' provided, add this to the output
if ("email" in fileDetailsFromOneDrive["lastModifiedBy"]["user"]) {
writeln("Email Address: ", fileDetailsFromOneDrive["lastModifiedBy"]["user"]["email"].str);
}
}
}
// --create-share-link response
if (outputType == "ShareableLink") {
JSONValue accessScope;
JSONValue createShareableLinkResponse;
string thisDriveId = fileDetailsFromOneDrive["parentReference"]["driveId"].str;
string thisItemId = fileDetailsFromOneDrive["id"].str;
string fileShareLink;
bool writeablePermissions = appConfig.getValueBool("with_editing_perms");
// What sort of shareable link is required?
if (writeablePermissions) {
// configure the read-write access scope
accessScope = [
"type": "edit",
"scope": "anonymous"
];
} else {
// configure the read-only access scope (default)
accessScope = [
"type": "view",
"scope": "anonymous"
];
}
// Try and create the shareable file link
try {
createShareableLinkResponse = queryOneDriveForFileDetailsApiInstance.createShareableLink(thisDriveId, thisItemId, accessScope);
} catch (OneDriveException exception) {
// display what the error is
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
return;
}
// Is the API response a valid JSON file?
if ((createShareableLinkResponse.type() == JSONType.object) && ("link" in createShareableLinkResponse)) {
// Extract the file share link from the JSON response
fileShareLink = createShareableLinkResponse["link"]["webUrl"].str;
writeln("File Shareable Link: ", fileShareLink);
if (writeablePermissions) {
writeln("Shareable Link has read-write permissions - use and provide with caution");
}
}
}
}
// Shutdown the API access
queryOneDriveForFileDetailsApiInstance.shutdown();
// Free object and memory
object.destroy(queryOneDriveForFileDetailsApiInstance);
}
}
// was path found?
if (!pathInDB) {
// File has not been synced with OneDrive
log.error("Selected path has not been synced with OneDrive: ", inputFilePath);
}
} else {
// File does not exist locally
log.error("Selected path not found on local system: ", inputFilePath);
}
}
}