Skip to content

Added the ability to set the storage location on iOS #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 60 additions & 20 deletions ios/RNCAsyncStorage.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,25 @@
#import "RCTLog.h"
#import "RCTUtils.h"

typedef NS_ENUM(NSInteger, RCTStorageLocation) {
Documents,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we conform Apple naming style?
image

ApplicationSupport

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krizzu Is this what we want? only support Documents and ApplicationSupport directories?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been discussed in #13, where ApplicationSupport is more safe to store app-specific data.

Do you have any suggestion if there should be more/less options to choose from?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krizzu Oh, I scan those things, so now I think we may need to handle two things:

  1. Provide options for user to save data. (I suppose support three options, Documents,Library,tmp, https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1, tmp is also meaningful IMO, provide temp storage).
  2. Not backed up by iTunes. (For the Application Support directory, it would backed up by iTunes and iCloud, so if we use that, we need use NSURLIsExcludedFromBackupKey for files we stored includes manifest file.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. not sure about tmp - do you have any use cases where it might be helpful? I think that would just cause confusion as tmp dir can be deleted deleted by OS at any time.
  2. maybe we can add a flag to control that? it could be useful to have this file backed up.

};

@implementation RCTConvert (RCTStorageLocation)

RCT_ENUM_CONVERTER(RCTStorageLocation, (@{
@"documents": @(Documents),
@"applicationSupport": @(ApplicationSupport),
}), Documents, integerValue)

@end

static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1";
static NSString *const RCTManifestFileName = @"manifest.json";
static const NSUInteger RCTInlineValueThreshold = 1024;
static NSString *manifestFilePath = nil;
static NSString *storageDirectory = nil;

#pragma mark - Static helper functions

Expand Down Expand Up @@ -66,29 +82,33 @@ static void RCTAppendError(NSDictionary *error, NSMutableArray<NSDictionary *> *
return nil;
}

static NSString *RCTGetStorageDirectory()
static NSString *RCTGetStorageDirectory(RCTStorageLocation storageLocation)
{
static NSString *storageDirectory = nil;
if (storageDirectory != nil) {
return storageDirectory;
}
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
#if TARGET_OS_TV
storageDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
#else
storageDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSSearchPathDirectory path = NSDocumentDirectory;
if (storageLocation == ApplicationSupport) {
path = NSApplicationSupportDirectory;
}
storageDirectory = NSSearchPathForDirectoriesInDomains(path, NSUserDomainMask, YES).firstObject;
#endif
storageDirectory = [storageDirectory stringByAppendingPathComponent:RCTStorageDirectory];
});
return storageDirectory;
}

static NSString *RCTGetManifestFilePath()
static NSString *RCTGetManifestFilePath(RCTStorageLocation storageLocation)
{
static NSString *manifestFilePath = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manifestFilePath = [RCTGetStorageDirectory() stringByAppendingPathComponent:RCTManifestFileName];
});
return manifestFilePath;
if (manifestFilePath != nil) {
return manifestFilePath;
}
return manifestFilePath = [RCTGetStorageDirectory(storageLocation) stringByAppendingPathComponent:RCTManifestFileName];;
}

// Only merges objects - all other types are just clobbered (including arrays)
Expand Down Expand Up @@ -148,14 +168,19 @@ static dispatch_queue_t RCTGetMethodQueue()
}

static BOOL RCTHasCreatedStorageDirectory = NO;
static NSDictionary *RCTDeleteStorageDirectory()
static NSDictionary *RCTDeleteStorageDirectory(RCTStorageLocation storageLocation)
{
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDirectory() error:&error];
[[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDirectory(storageLocation) error:&error];
RCTHasCreatedStorageDirectory = NO;
return error ? RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil;
}

static BOOL RCTStorageDirectoryExists(RCTStorageLocation storageLocation)
{
return [[NSFileManager defaultManager] fileExistsAtPath:RCTGetStorageDirectory(storageLocation)];
}

#pragma mark - RCTAsyncLocalStorage

@implementation RCTAsyncLocalStorage
Expand All @@ -165,6 +190,7 @@ @implementation RCTAsyncLocalStorage
// in separate files (as opposed to nil values which don't exist). The manifest is read off disk at startup, and
// written to disk after all mutations.
NSMutableDictionary<NSString *, NSString *> *_manifest;
RCTStorageLocation storageLocation;
}

RCT_EXPORT_MODULE()
Expand All @@ -179,25 +205,33 @@ - (void)clearAllData
dispatch_async(RCTGetMethodQueue(), ^{
[self->_manifest removeAllObjects];
[RCTGetCache() removeAllObjects];
RCTDeleteStorageDirectory();
RCTDeleteStorageDirectory(self->storageLocation);
});
}

+ (void)clearAllData
{
dispatch_async(RCTGetMethodQueue(), ^{
[RCTGetCache() removeAllObjects];
RCTDeleteStorageDirectory();
if (RCTStorageDirectoryExists(Documents)) {
RCTDeleteStorageDirectory(Documents);
}
if (RCTStorageDirectoryExists(ApplicationSupport)) {
RCTDeleteStorageDirectory(ApplicationSupport);
}
});
}

- (void)invalidate
{
if (_clearOnInvalidate) {
[RCTGetCache() removeAllObjects];
RCTDeleteStorageDirectory();
RCTDeleteStorageDirectory(storageLocation);
}
_clearOnInvalidate = NO;
manifestFilePath = nil;
storageDirectory = nil;
RCTHasCreatedStorageDirectory = false;
[_manifest removeAllObjects];
_haveSetup = NO;
}
Expand All @@ -215,7 +249,7 @@ - (void)dealloc
- (NSString *)_filePathForKey:(NSString *)key
{
NSString *safeFileName = RCTMD5Hash(key);
return [RCTGetStorageDirectory() stringByAppendingPathComponent:safeFileName];
return [RCTGetStorageDirectory(storageLocation) stringByAppendingPathComponent:safeFileName];
}

- (NSDictionary *)_ensureSetup
Expand All @@ -228,7 +262,7 @@ - (NSDictionary *)_ensureSetup

NSError *error = nil;
if (!RCTHasCreatedStorageDirectory) {
[[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory()
[[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory(storageLocation)
withIntermediateDirectories:YES
attributes:nil
error:&error];
Expand All @@ -239,7 +273,7 @@ - (NSDictionary *)_ensureSetup
}
if (!_haveSetup) {
NSDictionary *errorOut;
NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), RCTManifestFileName, &errorOut);
NSString *serialized = RCTReadFile(RCTGetManifestFilePath(storageLocation), RCTManifestFileName, &errorOut);
_manifest = serialized ? RCTJSONParseMutable(serialized, &error) : [NSMutableDictionary new];
if (error) {
RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error);
Expand All @@ -254,7 +288,7 @@ - (NSDictionary *)_writeManifest:(NSMutableArray<NSDictionary *> **)errors
{
NSError *error;
NSString *serialized = RCTJSONStringify(_manifest, &error);
[serialized writeToFile:RCTGetManifestFilePath() atomically:YES encoding:NSUTF8StringEncoding error:&error];
[serialized writeToFile:RCTGetManifestFilePath(storageLocation) atomically:YES encoding:NSUTF8StringEncoding error:&error];
NSDictionary *errorOut;
if (error) {
errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil);
Expand Down Expand Up @@ -441,7 +475,7 @@ - (NSDictionary *)_writeEntry:(NSArray<NSString *> *)entry changedManifest:(BOOL
{
[_manifest removeAllObjects];
[RCTGetCache() removeAllObjects];
NSDictionary *error = RCTDeleteStorageDirectory();
NSDictionary *error = RCTDeleteStorageDirectory(storageLocation);
callback(@[RCTNullIfNil(error)]);
}

Expand All @@ -455,4 +489,10 @@ - (NSDictionary *)_writeEntry:(NSArray<NSString *> *)entry changedManifest:(BOOL
}
}

RCT_EXPORT_METHOD(setStorageLocation:(RCTStorageLocation)location)
{
storageLocation = location;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have equal check in here.

[self invalidate];
}

@end
18 changes: 18 additions & 0 deletions lib/AsyncStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,24 @@ const AsyncStorage = {
});
});
},

/**
* Call this set the storage location.
* @param storageLocation The storage location. Can be 'documents' or 'applicationSupport'.
*
* **Note**: changing this only sets the location for future actions and does not migrate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit is bit worrying. We had similar issue #55, where storage location was changed and devs were losing data. Maybe we could come up with migration plan for data?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default locations is still the same, imo devs can only loose data once updated to a new version which contains this change AND change the storage location specifically without copying their data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks. We should provide a note in docs that this can happen, so they know upfront.

* the store from the previous location.
*
* @platform ios
*/
setStorageLocationIOS: function(storageLocation: string) {
RCTAsyncStorage.setStorageLocation(storageLocation);
},

StorageLocationIOS: {
documents: 'documents',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could export constants to be used in JS, what do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah that's a great shout! will do that

applicationSupport: 'applicationSupport',
},
};

// Not all native implementations support merge.
Expand Down