From deaa52e2f6aa8e8e5cc71e56189537d3316864d2 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 7 Apr 2023 18:34:15 +0300 Subject: [PATCH 01/11] Improvement on 'getIcloudDocument' --- ios/RNCloudFs.m | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index d38ff4e..7ced3be 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -23,7 +23,7 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_MODULE() //see https://developer.apple.com/library/content/documentation/General/Conceptual/iCloudDesignGuide/Chapters/iCloudFundametals.html - + RCT_EXPORT_METHOD(isAvailable:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -170,17 +170,30 @@ - (dispatch_queue_t)methodQueue } -RCT_EXPORT_METHOD(getIcloudDocument:(NSString *)filename +RCT_EXPORT_METHOD(getIcloudDocument:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter) { + NSString *destinationPath = [options objectForKey:@"targetPath"]; + NSString *scope = [options objectForKey:@"scope"]; + __block bool resolved = NO; _query = [[NSMetadataQuery alloc] init]; - [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]]; + bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; + + NSURL *ubiquityURL = documentsFolder ? [self icloudDocumentsDirectory] : [self icloudDirectory]; + NSURL* expectedURL = [ubiquityURL URLByAppendingPathComponent:destinationPath]; + + if(documentsFolder){ + [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope]]; + } else { + [_query setSearchScopes:@[NSMetadataQueryUbiquitousDataScope]]; + } + + NSString *filename = [destinationPath lastPathComponent]; NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemFSNameKey, filename]; [_query setPredicate:pred]; - [[NSNotificationCenter defaultCenter] addObserverForName: NSMetadataQueryDidFinishGatheringNotification @@ -191,7 +204,9 @@ - (dispatch_queue_t)methodQueue [query disableUpdates]; [query stopQuery]; for (NSMetadataItem *item in query.results) { - if([[item valueForAttribute:NSMetadataItemFSNameKey] isEqualToString:filename]){ + // NSString *relativePath = [[url path] stringByReplacingOccurrencesOfString:[ubiquityURL path] withString:@"."]; + NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; + if ([url isEqual:expectedURL]) { resolved = YES; NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; bool fileIsReady = [self downloadFileIfNotAvailable: item]; @@ -201,12 +216,12 @@ - (dispatch_queue_t)methodQueue return resolver(content); } else { // Call itself until the file it's ready - [self getIcloudDocument:filename resolver:resolver rejecter:rejecter]; + [self getIcloudDocument:options resolver:resolver rejecter:rejecter]; } } } if(!resolved){ - return rejecter(@"error", [NSString stringWithFormat:@"item not found '%@'", filename], nil); + return resolver(nil); } }]; @@ -220,7 +235,7 @@ - (dispatch_queue_t)methodQueue resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter) { NSError *error; - + NSFileManager* fileManager = [NSFileManager defaultManager]; [fileManager removeItemAtPath:item[@"path"] error:&error]; if(error) { @@ -433,21 +448,21 @@ - (BOOL)downloadFileIfNotAvailable:(NSMetadataItem*)item { RCT_EXPORT_METHOD(syncCloud:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - + _query = [[NSMetadataQuery alloc] init]; [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]]; [_query setPredicate:[NSPredicate predicateWithFormat: @"%K LIKE '*'", NSMetadataItemFSNameKey]]; dispatch_async(dispatch_get_main_queue(), ^{ - + BOOL startedQuery = [self->_query startQuery]; if (!startedQuery) { reject(@"error", @"Failed to start query.\n", nil); } }); - + [[NSNotificationCenter defaultCenter] addObserverForName: NSMetadataQueryDidFinishGatheringNotification object:_query queue:[NSOperationQueue currentQueue] @@ -461,7 +476,7 @@ - (BOOL)downloadFileIfNotAvailable:(NSMetadataItem*)item { } return resolve(@YES); }]; - + } @end From c6a482ef0fbbde1b88b23e6caa9462f8759a2236 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 12 Apr 2023 00:13:57 +0300 Subject: [PATCH 02/11] iOS. getIcloudDocument(). Recursion is made async --- ios/RNCloudFs.m | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index 7ced3be..9a95b11 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -182,7 +182,7 @@ - (dispatch_queue_t)methodQueue bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; NSURL *ubiquityURL = documentsFolder ? [self icloudDocumentsDirectory] : [self icloudDirectory]; - NSURL* expectedURL = [ubiquityURL URLByAppendingPathComponent:destinationPath]; + NSURL *expectedURL = [ubiquityURL URLByAppendingPathComponent:destinationPath]; if(documentsFolder){ [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope]]; @@ -204,20 +204,22 @@ - (dispatch_queue_t)methodQueue [query disableUpdates]; [query stopQuery]; for (NSMetadataItem *item in query.results) { - // NSString *relativePath = [[url path] stringByReplacingOccurrencesOfString:[ubiquityURL path] withString:@"."]; NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; + // NSString *relativePath = [[url path] stringByReplacingOccurrencesOfString:[ubiquityURL path] withString:@"."]; + // if ([[url path] hasSuffix:destinationPath]) { if ([url isEqual:expectedURL]) { resolved = YES; - NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; - bool fileIsReady = [self downloadFileIfNotAvailable: item]; + bool fileIsReady = [self startFileDownloadIfNotAvailable: item]; if(fileIsReady){ NSData *data = [NSData dataWithContentsOfURL: url]; NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; return resolver(content); - } else { - // Call itself until the file it's ready - [self getIcloudDocument:options resolver:resolver rejecter:rejecter]; } + // Call itself until the file is ready + RCTLogTrace(@"Waiting async 2s before retrying..."); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self getIcloudDocument:options resolver:resolver rejecter:rejecter]; + }); } } if(!resolved){ @@ -427,22 +429,22 @@ - (NSURL *)localPathForResource:(NSString *)resource ofType:(NSString *)type { -- (BOOL)downloadFileIfNotAvailable:(NSMetadataItem*)item { +- (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { if ([[item valueForAttribute:NSMetadataUbiquitousItemDownloadingStatusKey] isEqualToString:NSMetadataUbiquitousItemDownloadingStatusCurrent]){ NSLog(@"File is ready!"); return YES; } - // Download the file. - NSFileManager* fm = [NSFileManager defaultManager]; + + // Starting download + NSFileManager *fm = [NSFileManager defaultManager]; NSError *downloadError = nil; [fm startDownloadingUbiquitousItemAtURL:[item valueForAttribute:NSMetadataItemURLKey] error:&downloadError]; + if (downloadError) { NSLog(@"Error occurred starting download: %@", downloadError); } - NSLog(@"Waiting before retrying..."); - [NSThread sleepForTimeInterval:0.3]; - return NO; + return NO; } @@ -472,7 +474,7 @@ - (BOOL)downloadFileIfNotAvailable:(NSMetadataItem*)item { [query disableUpdates]; [query stopQuery]; for (NSMetadataItem *item in query.results) { - [self downloadFileIfNotAvailable: item]; + [self startFileDownloadIfNotAvailable: item]; } return resolve(@YES); }]; From 1e87c46952bdbc5a67272460676cac5d96f8ffba Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 12 Apr 2023 00:14:12 +0300 Subject: [PATCH 03/11] + types.d.ts --- package.json | 1 + types.d.ts | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 types.d.ts diff --git a/package.json b/package.json index ec47698..66ee88f 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "2.6.0", "description": "", "main": "index.js", + "types": "types.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..7a0c27e --- /dev/null +++ b/types.d.ts @@ -0,0 +1,98 @@ +/** Defaults to `visible` */ +export type Scope = 'visible' | 'hidden'; + +export interface CloudFileDetailsBase { + id: string; + name: string; + /** ISO */ + lastModified: string; +} + +export type GoogleDriveFileDetails = CloudFileDetailsBase; + +export interface ICloudFileDetails extends CloudFileDetailsBase { + isFile: boolean; + isDirectory: boolean; + path: string; + size?: number; + uri?: string; +} + +export interface TargetPathAndScope { + scope: Scope; + targetPath: string; +} + +declare const defaultExport: { + /** iOS only */ + isAvailable: () => Promise; + + /** Android only */ + loginIfNeeded: () => Promise; + + requestSignIn: () => void; + + /** iOS only + * + * (!) Flawed - doesn't help in one call. + * `listFiles` won't be always ready to return valid info + * + * (!) Won't return if `isAvailable` returns false + * + * (i) Attempts to load all the iCloud Drive files + */ + syncCloud: () => Promise; + + logout: () => Promise; + + /** + * (!) Broken - does nothing + * + * (!) Don't await - won't return. + */ + reset: () => Promise; + + // getConstants: + + /** + * (!) Won't return for Android, if not signed-in via `loginIfNeeded` + */ + listFiles: (options: TargetPathAndScope) => Promise< + | { + files?: GoogleDriveFileDetails[]; + } + | { + files?: ICloudFileDetails[]; + path: string; + } + >; + + /** + * @returns fileId: string // id for Android & absolute path for iOS + */ + copyToCloud: ( + options: TargetPathAndScope & { + mimeType: string; + sourcePath: { path: string } | { uri: string }; + } + ) => Promise; + + fileExists: ( + options: + | TargetPathAndScope // iOS + | { + // Android + scope: Scope; + fileId: string; + } + ) => Promise; + + deleteFromCloud: (fileId: string) => Promise; + + getGoogleDriveDocument: (fileId: string) => Promise; + + /** iOS only */ + getIcloudDocument: (options: TargetPathAndScope) => Promise; +}; + +export default defaultExport; From c3e3fa2315badea63c2ebcfaa67a64528da656c1 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 12 Apr 2023 05:37:22 +0300 Subject: [PATCH 04/11] iOS. getIcloudDocument(). Recursion is limited to 60s --- ios/RNCloudFs.m | 19 +++++++++++++++++-- types.d.ts | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index 9a95b11..485c6dc 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -173,6 +173,19 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_METHOD(getIcloudDocument:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter) { + [self getIcloudDocumentRecurse:options resolver:resolver rejecter:rejecter retryCount:1]; +} + +- (void)getIcloudDocumentRecurse:(NSDictionary *)options +resolver:(RCTPromiseResolveBlock)resolver +rejecter:(RCTPromiseRejectBlock)rejecter +retryCount:(int)retryCount { + if (retryCount > 30) { + NSString *errMsg = @"Failed to read document in 60 seconds"; + RCTLogTrace(errMsg); + return rejecter(@"error", errMsg, nil); + } + NSString *destinationPath = [options objectForKey:@"targetPath"]; NSString *scope = [options objectForKey:@"scope"]; @@ -218,8 +231,9 @@ - (dispatch_queue_t)methodQueue // Call itself until the file is ready RCTLogTrace(@"Waiting async 2s before retrying..."); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self getIcloudDocument:options resolver:resolver rejecter:rejecter]; + [self getIcloudDocumentRecurse:options resolver:resolver rejecter:rejecter retryCount:retryCount+1]; }); + break; } } if(!resolved){ @@ -430,7 +444,8 @@ - (NSURL *)localPathForResource:(NSString *)resource ofType:(NSString *)type { - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { - if ([[item valueForAttribute:NSMetadataUbiquitousItemDownloadingStatusKey] isEqualToString:NSMetadataUbiquitousItemDownloadingStatusCurrent]){ + NSString *downloadingStatus = [item valueForAttribute:NSMetadataUbiquitousItemDownloadingStatusKey]; + if ([downloadingStatus isEqualToString:NSMetadataUbiquitousItemDownloadingStatusCurrent]) { NSLog(@"File is ready!"); return YES; } diff --git a/types.d.ts b/types.d.ts index 7a0c27e..7dad741 100644 --- a/types.d.ts +++ b/types.d.ts @@ -23,7 +23,7 @@ export interface TargetPathAndScope { targetPath: string; } -declare const defaultExport: { +declare const defaultExport: Readonly<{ /** iOS only */ isAvailable: () => Promise; @@ -93,6 +93,6 @@ declare const defaultExport: { /** iOS only */ getIcloudDocument: (options: TargetPathAndScope) => Promise; -}; +}>; export default defaultExport; From ec6a3bfeae345d60dac6f653bc674c263fd2a27c Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 13 Apr 2023 19:46:43 +0300 Subject: [PATCH 05/11] iOS. getIcloudDocument(). Restricted to querying for single item --- ios/RNCloudFs.m | 87 ++++++++++++++++++++++++++----------------------- types.d.ts | 8 +++-- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index 485c6dc..e8ce950 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -170,16 +170,20 @@ - (dispatch_queue_t)methodQueue } -RCT_EXPORT_METHOD(getIcloudDocument:(NSDictionary *)options -resolver:(RCTPromiseResolveBlock)resolver -rejecter:(RCTPromiseRejectBlock)rejecter) { - [self getIcloudDocumentRecurse:options resolver:resolver rejecter:rejecter retryCount:1]; +RCT_EXPORT_METHOD(getIcloudDocument + :(NSDictionary *)options + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter +) { + [self getIcloudDocumentRecurse:options :resolver :rejecter :1]; } -- (void)getIcloudDocumentRecurse:(NSDictionary *)options -resolver:(RCTPromiseResolveBlock)resolver -rejecter:(RCTPromiseRejectBlock)rejecter -retryCount:(int)retryCount { +- (void)getIcloudDocumentRecurse + :(NSDictionary *)options + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter + :(int)retryCount +{ if (retryCount > 30) { NSString *errMsg = @"Failed to read document in 60 seconds"; RCTLogTrace(errMsg); @@ -189,60 +193,62 @@ - (void)getIcloudDocumentRecurse:(NSDictionary *)options NSString *destinationPath = [options objectForKey:@"targetPath"]; NSString *scope = [options objectForKey:@"scope"]; - __block bool resolved = NO; - _query = [[NSMetadataQuery alloc] init]; + NSMetadataQuery *_query = [[NSMetadataQuery alloc] init]; bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; - NSURL *ubiquityURL = documentsFolder ? [self icloudDocumentsDirectory] : [self icloudDirectory]; - NSURL *expectedURL = [ubiquityURL URLByAppendingPathComponent:destinationPath]; - if(documentsFolder){ [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope]]; } else { [_query setSearchScopes:@[NSMetadataQueryUbiquitousDataScope]]; } - NSString *filename = [destinationPath lastPathComponent]; + NSURL *ubiquityURL = documentsFolder ? [self icloudDocumentsDirectory] : [self icloudDirectory]; + NSURL *expectedURL = [ubiquityURL URLByAppendingPathComponent:destinationPath]; + // NSString *relativePath = [[url path] stringByReplacingOccurrencesOfString:[ubiquityURL path] withString:@"."]; + NSString *expectedPath = [expectedURL path]; - NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemFSNameKey, filename]; + NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemPathKey, expectedPath]; [_query setPredicate:pred]; - [[NSNotificationCenter defaultCenter] addObserverForName: - NSMetadataQueryDidFinishGatheringNotification - object:_query queue:[NSOperationQueue currentQueue] - usingBlock:^(NSNotification __strong *notification) + [[NSNotificationCenter defaultCenter] addObserverForName:NSMetadataQueryDidFinishGatheringNotification + object:_query + queue:[NSOperationQueue currentQueue] + usingBlock:^(NSNotification __strong *notification) { NSMetadataQuery *query = [notification object]; [query disableUpdates]; [query stopQuery]; - for (NSMetadataItem *item in query.results) { + // _query = nil; + + if ([query resultCount] < 1) { + return resolver(nil); + } else if ([query resultCount] == 1) { + NSMetadataItem *item = [query resultAtIndex:0]; NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; - // NSString *relativePath = [[url path] stringByReplacingOccurrencesOfString:[ubiquityURL path] withString:@"."]; - // if ([[url path] hasSuffix:destinationPath]) { - if ([url isEqual:expectedURL]) { - resolved = YES; - bool fileIsReady = [self startFileDownloadIfNotAvailable: item]; - if(fileIsReady){ - NSData *data = [NSData dataWithContentsOfURL: url]; - NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - return resolver(content); - } - // Call itself until the file is ready - RCTLogTrace(@"Waiting async 2s before retrying..."); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self getIcloudDocumentRecurse:options resolver:resolver rejecter:rejecter retryCount:retryCount+1]; - }); - break; + bool fileIsReady = [self startFileDownloadIfNotAvailable: item]; + if(fileIsReady){ + NSData *data = [NSData dataWithContentsOfURL: url]; + NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + return resolver(content); } - } - if(!resolved){ - return resolver(nil); + // Call itself until the file is ready + RCTLogTrace(@"Waiting async 2s before retrying..."); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self getIcloudDocumentRecurse:options :resolver :rejecter :retryCount+1]; + }); + } else { + return rejecter(@"error", @"Found multiple documents", nil); } }]; dispatch_async(dispatch_get_main_queue(), ^{ - [self->_query startQuery]; + + BOOL startedQuery = [_query startQuery]; + if (!startedQuery) + { + rejecter(@"error", @"Failed to start query", nil); + } }); } @@ -442,7 +448,6 @@ - (NSURL *)localPathForResource:(NSString *)resource ofType:(NSString *)type { } - - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { NSString *downloadingStatus = [item valueForAttribute:NSMetadataUbiquitousItemDownloadingStatusKey]; if ([downloadingStatus isEqualToString:NSMetadataUbiquitousItemDownloadingStatusCurrent]) { diff --git a/types.d.ts b/types.d.ts index 7dad741..49fa9ca 100644 --- a/types.d.ts +++ b/types.d.ts @@ -30,6 +30,7 @@ declare const defaultExport: Readonly<{ /** Android only */ loginIfNeeded: () => Promise; + /** Android only */ requestSignIn: () => void; /** iOS only @@ -43,9 +44,10 @@ declare const defaultExport: Readonly<{ */ syncCloud: () => Promise; + /** Android only */ logout: () => Promise; - /** + /** Android only * (!) Broken - does nothing * * (!) Don't await - won't return. @@ -56,6 +58,8 @@ declare const defaultExport: Readonly<{ /** * (!) Won't return for Android, if not signed-in via `loginIfNeeded` + * + * (!) Accounts only for already downloaded files for iOS */ listFiles: (options: TargetPathAndScope) => Promise< | { @@ -87,7 +91,7 @@ declare const defaultExport: Readonly<{ } ) => Promise; - deleteFromCloud: (fileId: string) => Promise; + // deleteFromCloud: (fileId: string) => Promise; getGoogleDriveDocument: (fileId: string) => Promise; From 011299d025ece2eff37032b62d0bef233e7f6c82 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 13 Apr 2023 20:23:47 +0300 Subject: [PATCH 06/11] iOS. syncCloud -> startIcloudSync --- ios/RNCloudFs.m | 41 +++++++++++++++++++++++++++-------------- types.d.ts | 18 +++++++++--------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index e8ce950..45cf866 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -24,14 +24,20 @@ - (dispatch_queue_t)methodQueue //see https://developer.apple.com/library/content/documentation/General/Conceptual/iCloudDesignGuide/Chapters/iCloudFundametals.html -RCT_EXPORT_METHOD(isAvailable:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(isAvailable + :(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject +) { + bool isAvailable = [self isIcloudAvailable]; + return resolve(@(isAvailable)); +} +// Synchronous check +- (BOOL)isIcloudAvailable { NSURL *ubiquityURL = [self icloudDirectory]; - if(ubiquityURL != nil){ - return resolve(@YES); - } - return resolve(@NO); + + bool isAvailable = ubiquityURL != nil; + return @(isAvailable); } RCT_EXPORT_METHOD(createFile:(NSDictionary *) options @@ -211,7 +217,8 @@ - (void)getIcloudDocumentRecurse NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemPathKey, expectedPath]; [_query setPredicate:pred]; - [[NSNotificationCenter defaultCenter] addObserverForName:NSMetadataQueryDidFinishGatheringNotification + [[NSNotificationCenter defaultCenter] addObserverForName: + NSMetadataQueryDidFinishGatheringNotification object:_query queue:[NSOperationQueue currentQueue] usingBlock:^(NSNotification __strong *notification) @@ -468,17 +475,22 @@ - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { } -RCT_EXPORT_METHOD(syncCloud:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(startIcloudSync + :(RCTPromiseResolveBlock)resolve + :(RCTPromiseRejectBlock)reject +) { + if (![self isIcloudAvailable]) { + reject(@"error", @"iCloud is not available", nil); + } - _query = [[NSMetadataQuery alloc] init]; + NSMetadataQuery *_query = [[NSMetadataQuery alloc] init]; [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]]; [_query setPredicate:[NSPredicate predicateWithFormat: @"%K LIKE '*'", NSMetadataItemFSNameKey]]; dispatch_async(dispatch_get_main_queue(), ^{ - BOOL startedQuery = [self->_query startQuery]; + BOOL startedQuery = [_query startQuery]; if (!startedQuery) { reject(@"error", @"Failed to start query.\n", nil); @@ -486,9 +498,10 @@ - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { }); [[NSNotificationCenter defaultCenter] addObserverForName: - NSMetadataQueryDidFinishGatheringNotification - object:_query queue:[NSOperationQueue currentQueue] - usingBlock:^(NSNotification __strong *notification) + NSMetadataQueryDidFinishGatheringNotification + object:_query + queue:[NSOperationQueue currentQueue] + usingBlock:^(NSNotification __strong *notification) { NSMetadataQuery *query = [notification object]; [query disableUpdates]; diff --git a/types.d.ts b/types.d.ts index 49fa9ca..7f646f9 100644 --- a/types.d.ts +++ b/types.d.ts @@ -33,16 +33,13 @@ declare const defaultExport: Readonly<{ /** Android only */ requestSignIn: () => void; - /** iOS only - * - * (!) Flawed - doesn't help in one call. - * `listFiles` won't be always ready to return valid info - * - * (!) Won't return if `isAvailable` returns false + /** + * (i) Only initiates syncing. + * Files won't necessarily be immediately ready to use * - * (i) Attempts to load all the iCloud Drive files + * (!) Attempts to load all the iCloud Drive files */ - syncCloud: () => Promise; + startIcloudSync: () => Promise; /** Android only */ logout: () => Promise; @@ -59,7 +56,7 @@ declare const defaultExport: Readonly<{ /** * (!) Won't return for Android, if not signed-in via `loginIfNeeded` * - * (!) Accounts only for already downloaded files for iOS + * (!) Accounts only for locally present files for iOS */ listFiles: (options: TargetPathAndScope) => Promise< | { @@ -81,6 +78,9 @@ declare const defaultExport: Readonly<{ } ) => Promise; + /** + * (!) Accounts only for locally present files for iOS + */ fileExists: ( options: | TargetPathAndScope // iOS From dc2d83d249453d0535a88de84efc64f76a08020c Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 13 Apr 2023 23:50:23 +0300 Subject: [PATCH 07/11] iOS. + getIcloudDocumentDetails() --- ios/RNCloudFs.m | 96 ++++++++++++++++++++++++++++++++++++++----------- types.d.ts | 34 +++++++++++------- 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index 45cf866..16febb1 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -199,7 +199,7 @@ - (void)getIcloudDocumentRecurse NSString *destinationPath = [options objectForKey:@"targetPath"]; NSString *scope = [options objectForKey:@"scope"]; - NSMetadataQuery *_query = [[NSMetadataQuery alloc] init]; + _query = [[NSMetadataQuery alloc] init]; bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; @@ -226,40 +226,94 @@ - (void)getIcloudDocumentRecurse NSMetadataQuery *query = [notification object]; [query disableUpdates]; [query stopQuery]; - // _query = nil; if ([query resultCount] < 1) { return resolver(nil); - } else if ([query resultCount] == 1) { - NSMetadataItem *item = [query resultAtIndex:0]; - NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; - bool fileIsReady = [self startFileDownloadIfNotAvailable: item]; - if(fileIsReady){ - NSData *data = [NSData dataWithContentsOfURL: url]; - NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - return resolver(content); - } - // Call itself until the file is ready - RCTLogTrace(@"Waiting async 2s before retrying..."); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self getIcloudDocumentRecurse:options :resolver :rejecter :retryCount+1]; - }); - } else { + } else if ([query resultCount] > 1) { return rejecter(@"error", @"Found multiple documents", nil); } + + NSMetadataItem *item = [query resultAtIndex:0]; + NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; + bool fileIsReady = [self startFileDownloadIfNotAvailable: item]; + if(fileIsReady){ + NSData *data = [NSData dataWithContentsOfURL: url]; + NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + return resolver(content); + } + // Call itself until the file is ready + RCTLogTrace(@"Waiting async 2s before retrying..."); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self getIcloudDocumentRecurse:options :resolver :rejecter :retryCount+1]; + }); }]; dispatch_async(dispatch_get_main_queue(), ^{ - - BOOL startedQuery = [_query startQuery]; + BOOL startedQuery = [self->_query startQuery]; if (!startedQuery) { rejecter(@"error", @"Failed to start query", nil); } }); +} + +RCT_EXPORT_METHOD(getIcloudDocumentDetails + :(NSDictionary *)options + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter +) { + NSString *destinationPath = [options objectForKey:@"targetPath"]; + NSString *scope = [options objectForKey:@"scope"]; + + _query = [[NSMetadataQuery alloc] init]; + bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; + + if(documentsFolder){ + [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope]]; + } else { + [_query setSearchScopes:@[NSMetadataQueryUbiquitousDataScope]]; + } + + NSURL *ubiquityURL = documentsFolder ? [self icloudDocumentsDirectory] : [self icloudDirectory]; + NSURL *expectedURL = [ubiquityURL URLByAppendingPathComponent:destinationPath]; + NSString *expectedPath = [expectedURL path]; + + NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemPathKey, expectedPath]; + [_query setPredicate:pred]; + + [[NSNotificationCenter defaultCenter] addObserverForName: + NSMetadataQueryDidFinishGatheringNotification + object:_query + queue:[NSOperationQueue currentQueue] + usingBlock:^(NSNotification __strong *notification) + { + NSMetadataQuery *query = [notification object]; + [query disableUpdates]; + [query stopQuery]; + + if ([query resultCount] < 1) { + return resolver(nil); + } else if ([query resultCount] > 1) { + return rejecter(@"error", @"Found multiple documents", nil); + } + + NSMetadataItem *item = [query resultAtIndex:0]; + resolver(@{ + @"downloadingStatus": [item valueForAttribute:NSMetadataUbiquitousItemDownloadingStatusKey] + }); + }]; + + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL startedQuery = [self->_query startQuery]; + if (!startedQuery) + { + rejecter(@"error", @"Failed to start query", nil); + } + }); } + RCT_EXPORT_METHOD(deleteFromCloud:(NSDictionary *)item resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter) { @@ -483,14 +537,14 @@ - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { reject(@"error", @"iCloud is not available", nil); } - NSMetadataQuery *_query = [[NSMetadataQuery alloc] init]; + _query = [[NSMetadataQuery alloc] init]; [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]]; [_query setPredicate:[NSPredicate predicateWithFormat: @"%K LIKE '*'", NSMetadataItemFSNameKey]]; dispatch_async(dispatch_get_main_queue(), ^{ - BOOL startedQuery = [_query startQuery]; + BOOL startedQuery = [self->_query startQuery]; if (!startedQuery) { reject(@"error", @"Failed to start query.\n", nil); diff --git a/types.d.ts b/types.d.ts index 7f646f9..0f4722e 100644 --- a/types.d.ts +++ b/types.d.ts @@ -2,13 +2,14 @@ export type Scope = 'visible' | 'hidden'; export interface CloudFileDetailsBase { - id: string; name: string; /** ISO */ lastModified: string; } -export type GoogleDriveFileDetails = CloudFileDetailsBase; +export interface GoogleDriveFileDetails extends CloudFileDetailsBase { + id: string; +}; export interface ICloudFileDetails extends CloudFileDetailsBase { isFile: boolean; @@ -18,11 +19,17 @@ export interface ICloudFileDetails extends CloudFileDetailsBase { uri?: string; } +export interface ICloudDocumentDetails { + downloadingStatus: number; +} + export interface TargetPathAndScope { scope: Scope; targetPath: string; } +export default defaultExport; + declare const defaultExport: Readonly<{ /** iOS only */ isAvailable: () => Promise; @@ -58,18 +65,20 @@ declare const defaultExport: Readonly<{ * * (!) Accounts only for locally present files for iOS */ - listFiles: (options: TargetPathAndScope) => Promise< - | { - files?: GoogleDriveFileDetails[]; - } - | { - files?: ICloudFileDetails[]; + listFiles:

(options: TargetPathAndScope) => Promise< + P extends 'iOS' + ? { + files: ICloudFileDetails[]; + /** Relative hosting dir path */ path: string; } + : { + files?: GoogleDriveFileDetails[]; + } >; /** - * @returns fileId: string // id for Android & absolute path for iOS + * @returns fileId: string // id for Android & path for iOS */ copyToCloud: ( options: TargetPathAndScope & { @@ -93,10 +102,11 @@ declare const defaultExport: Readonly<{ // deleteFromCloud: (fileId: string) => Promise; - getGoogleDriveDocument: (fileId: string) => Promise; + getIcloudDocumentDetails: (options: TargetPathAndScope) => Promise; - /** iOS only */ getIcloudDocument: (options: TargetPathAndScope) => Promise; + + getGoogleDriveDocument: (fileId: string) => Promise; }>; -export default defaultExport; +type Platform = 'Android' | 'iOS'; From 006eec31b61ffe1640fce8c52a6b2336504c997f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Apr 2023 00:01:42 +0300 Subject: [PATCH 08/11] iOS. + '_query = nil;' --- ios/RNCloudFs.m | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index 16febb1..614074d 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -226,6 +226,7 @@ - (void)getIcloudDocumentRecurse NSMetadataQuery *query = [notification object]; [query disableUpdates]; [query stopQuery]; + _query = nil; if ([query resultCount] < 1) { return resolver(nil); @@ -291,6 +292,7 @@ - (void)getIcloudDocumentRecurse NSMetadataQuery *query = [notification object]; [query disableUpdates]; [query stopQuery]; + _query = nil; if ([query resultCount] < 1) { return resolver(nil); @@ -541,16 +543,6 @@ - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { [_query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]]; [_query setPredicate:[NSPredicate predicateWithFormat: @"%K LIKE '*'", NSMetadataItemFSNameKey]]; - - dispatch_async(dispatch_get_main_queue(), ^{ - - BOOL startedQuery = [self->_query startQuery]; - if (!startedQuery) - { - reject(@"error", @"Failed to start query.\n", nil); - } - }); - [[NSNotificationCenter defaultCenter] addObserverForName: NSMetadataQueryDidFinishGatheringNotification object:_query @@ -560,12 +552,22 @@ - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { NSMetadataQuery *query = [notification object]; [query disableUpdates]; [query stopQuery]; + _query = nil; + for (NSMetadataItem *item in query.results) { [self startFileDownloadIfNotAvailable: item]; } + return resolve(@YES); }]; + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL startedQuery = [self->_query startQuery]; + if (!startedQuery) + { + reject(@"error", @"Failed to start query.\n", nil); + } + }); } @end From ee38534accf2a2c091cf43ef5061fec255e95687 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Apr 2023 23:16:10 +0300 Subject: [PATCH 09/11] iOS. + Key-Value-Store methods --- ios/RNCloudFs.h | 1 + ios/RNCloudFs.m | 84 ++++++++++++++++++++++++++++++++++++++++++++++++- types.d.ts | 28 ++++++++++++++++- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/ios/RNCloudFs.h b/ios/RNCloudFs.h index f6bf5c4..e0e38f2 100644 --- a/ios/RNCloudFs.h +++ b/ios/RNCloudFs.h @@ -7,4 +7,5 @@ @interface RNCloudFs : NSObject @property (nonatomic, strong) NSMetadataQuery *query; +@property (nonatomic, strong) NSUbiquitousKeyValueStore *iCloudStore; @end diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index 614074d..21660a8 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -302,7 +302,13 @@ - (void)getIcloudDocumentRecurse NSMetadataItem *item = [query resultAtIndex:0]; resolver(@{ - @"downloadingStatus": [item valueForAttribute:NSMetadataUbiquitousItemDownloadingStatusKey] + @"fileStatus": @{ + @"downloading": [item valueForAttribute:NSMetadataUbiquitousItemDownloadingStatusKey], + @"isDownloading": [item valueForAttribute:NSMetadataUbiquitousItemIsDownloadingKey], + @"isUploading": [item valueForAttribute:NSMetadataUbiquitousItemIsUploadingKey], + @"percentDownloaded": [item valueForAttribute:NSMetadataUbiquitousItemPercentDownloadedKey], + @"percentUploaded": [item valueForAttribute:NSMetadataUbiquitousItemPercentUploadedKey] + } }); }]; @@ -570,4 +576,80 @@ - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { }); } + +//// # NSUbiquitousKeyValueStore + + +RCT_EXPORT_METHOD(getKeyValueStoreObject + :(NSString *)key + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter +) { + NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore]; + + NSString *value = [iCloudStore objectForKey:key]; + if (value) { + resolver(value); + } else { + resolver(nil); + } +} + +RCT_EXPORT_METHOD(getKeyValueStoreObjectDetails + :(NSString *)key + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter +) { + NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore]; + + NSString *value = [iCloudStore objectForKey:key]; + if (value) { + resolver(@{ + @"valueLength": @(value.length) + }); + } else { + resolver(nil); + } +} + +RCT_EXPORT_METHOD(putKeyValueStoreObject + :(NSDictionary *)options + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter +) { + NSString *key = [options objectForKey:@"key"]; + NSString *value = [options objectForKey:@"value"]; + + NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore]; + [iCloudStore setObject:value forKey:key]; + + resolver(nil); +} + +// See: https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore/1415989-synchronize +RCT_EXPORT_METHOD(syncKeyValueStoreData + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter +) { + NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore]; + bool done = [iCloudStore synchronize]; + + resolver(@(done)); +} + +RCT_EXPORT_METHOD(removeKeyValueStoreObject + :(NSString *)key + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter +) { + NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore]; + + [iCloudStore removeObjectForKey:key]; + + bool done = [iCloudStore synchronize]; + + resolver(@(done)); +} + + @end diff --git a/types.d.ts b/types.d.ts index 0f4722e..2cc7182 100644 --- a/types.d.ts +++ b/types.d.ts @@ -20,7 +20,13 @@ export interface ICloudFileDetails extends CloudFileDetailsBase { } export interface ICloudDocumentDetails { - downloadingStatus: number; + fileStatus: { + downloading: string, + isDownloading: boolean, + isUploading: boolean, + percentDownloaded: number, + percentUploaded: number, + } } export interface TargetPathAndScope { @@ -107,6 +113,26 @@ declare const defaultExport: Readonly<{ getIcloudDocument: (options: TargetPathAndScope) => Promise; getGoogleDriveDocument: (fileId: string) => Promise; + + ////// # Key-Value-Store + + getKeyValueStoreObject: (key: string) => Promise; + + getKeyValueStoreObjectDetails: (key: string) => Promise<{ + valueLength: number; + } | undefined>; + + /** + * (i) Single value size limit is 4KB; Total limit is 64KB + */ + putKeyValueStoreObject: (data: { key: string; value: string }) => Promise; + + /** + * See: https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore/1415989-synchronize + */ + syncKeyValueStoreData: () => Promise; + + removeKeyValueStoreObject: (key: string) => Promise; }>; type Platform = 'Android' | 'iOS'; From 4974fd4111af3cf751ab782a2abb047b073f7d89 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 18 Apr 2023 22:18:55 +0300 Subject: [PATCH 10/11] iOS. Key-Value-Store. Refactor --- ios/RNCloudFs.m | 5 +---- types.d.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ios/RNCloudFs.m b/ios/RNCloudFs.m index 21660a8..82f9fd0 100644 --- a/ios/RNCloudFs.m +++ b/ios/RNCloudFs.m @@ -579,7 +579,6 @@ - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { //// # NSUbiquitousKeyValueStore - RCT_EXPORT_METHOD(getKeyValueStoreObject :(NSString *)key :(RCTPromiseResolveBlock)resolver @@ -646,9 +645,7 @@ - (BOOL)startFileDownloadIfNotAvailable:(NSMetadataItem*)item { [iCloudStore removeObjectForKey:key]; - bool done = [iCloudStore synchronize]; - - resolver(@(done)); + resolver(nil); } diff --git a/types.d.ts b/types.d.ts index 2cc7182..f3130dc 100644 --- a/types.d.ts +++ b/types.d.ts @@ -128,11 +128,16 @@ declare const defaultExport: Readonly<{ putKeyValueStoreObject: (data: { key: string; value: string }) => Promise; /** - * See: https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore/1415989-synchronize + * Syncs in-memory and disk key-value storages. + * Later will be orkestrated for upload to iCloud. + * + * See: + * - https://developer.apple.com/documentation/foundation/icloud/synchronizing_app_preferences_with_icloud + * - https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore/1415989-synchronize */ syncKeyValueStoreData: () => Promise; - removeKeyValueStoreObject: (key: string) => Promise; + removeKeyValueStoreObject: (key: string) => Promise; }>; type Platform = 'Android' | 'iOS'; From 1d64024c39e529c1d4c46ca9996e42a2d80bf67f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Sep 2023 01:14:08 +0300 Subject: [PATCH 11/11] Android. Configured to build with latest react-native --- android/build.gradle | 16 +++++++++------- types.d.ts | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 2494780..a6fd15d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,7 @@ buildscript { + ext.safeExtGet = {prop, fallback -> + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback + } repositories { jcenter() } @@ -10,12 +13,12 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 28 - buildToolsVersion "28.0.3" + compileSdkVersion safeExtGet('compileSdkVersion', 28) + buildToolsVersion safeExtGet('buildToolsVersion', "28.0.3") defaultConfig { minSdkVersion 19 - targetSdkVersion 28 + targetSdkVersion safeExtGet('targetSdkVersion', 28) versionCode 1 versionName "1.0" ndk { @@ -29,7 +32,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - + packagingOptions { exclude 'META-INF/DEPENDENCIES' } @@ -43,9 +46,8 @@ allprojects { dependencies { implementation 'com.google.android.gms:play-services-drive:17.0.0' - implementation ('com.google.android.gms:play-services-auth:17.0.0') { - force = true; - } + implementation 'com.google.android.gms:play-services-auth:17.0.0' + implementation 'com.google.http-client:google-http-client-gson:1.26.0' implementation('com.google.api-client:google-api-client-android:1.26.0') { exclude group: 'org.apache.httpcomponents' diff --git a/types.d.ts b/types.d.ts index f3130dc..057ee45 100644 --- a/types.d.ts +++ b/types.d.ts @@ -9,7 +9,7 @@ export interface CloudFileDetailsBase { export interface GoogleDriveFileDetails extends CloudFileDetailsBase { id: string; -}; +} export interface ICloudFileDetails extends CloudFileDetailsBase { isFile: boolean;