2015-03-23 20:28:42 +00:00
|
|
|
/**
|
|
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
|
|
|
* This source code is licensed under the BSD-style license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
|
|
*/
|
2015-03-11 19:06:03 +00:00
|
|
|
|
|
|
|
#import "RCTAsyncLocalStorage.h"
|
|
|
|
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
|
|
|
|
#import <CommonCrypto/CommonCryptor.h>
|
|
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
|
|
|
|
|
|
#import "RCTLog.h"
|
|
|
|
#import "RCTUtils.h"
|
|
|
|
|
|
|
|
static NSString *const kStorageDir = @"RCTAsyncLocalStorage_V1";
|
|
|
|
static NSString *const kManifestFilename = @"manifest.json";
|
|
|
|
static const NSUInteger kInlineValueThreshold = 100;
|
|
|
|
|
|
|
|
#pragma mark - Static helper functions
|
|
|
|
|
|
|
|
static id RCTErrorForKey(NSString *key)
|
|
|
|
{
|
|
|
|
if (![key isKindOfClass:[NSString class]]) {
|
|
|
|
return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key});
|
|
|
|
} else if (key.length < 1) {
|
|
|
|
return RCTMakeAndLogError(@"Invalid key - must be at least one character. Key: ", key, @{@"key": key});
|
|
|
|
} else {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void RCTAppendError(id error, NSMutableArray **errors)
|
|
|
|
{
|
|
|
|
if (error && errors) {
|
|
|
|
if (!*errors) {
|
|
|
|
*errors = [NSMutableArray new];
|
|
|
|
}
|
|
|
|
[*errors addObject:error];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut)
|
|
|
|
{
|
|
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
|
|
|
|
NSError *error;
|
|
|
|
NSStringEncoding encoding;
|
|
|
|
NSString *entryString = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error];
|
|
|
|
if (error) {
|
|
|
|
*errorOut = RCTMakeError(@"Failed to read storage file.", error, @{@"key": key});
|
|
|
|
} else if (encoding != NSUTF8StringEncoding) {
|
|
|
|
*errorOut = RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), @{@"key": key});
|
|
|
|
} else {
|
|
|
|
return entryString;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2015-06-03 23:57:08 +00:00
|
|
|
// Only merges objects - all other types are just clobbered (including arrays)
|
|
|
|
static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source)
|
|
|
|
{
|
|
|
|
for (NSString *key in source) {
|
|
|
|
id sourceValue = source[key];
|
|
|
|
if ([sourceValue isKindOfClass:[NSDictionary class]]) {
|
|
|
|
id destinationValue = destination[key];
|
|
|
|
NSMutableDictionary *nestedDestination;
|
|
|
|
if ([destinationValue classForCoder] == [NSMutableDictionary class]) {
|
|
|
|
nestedDestination = destinationValue;
|
|
|
|
} else {
|
|
|
|
if ([destinationValue isKindOfClass:[NSDictionary class]]) {
|
|
|
|
// Ideally we wouldn't eagerly copy here...
|
|
|
|
nestedDestination = [destinationValue mutableCopy];
|
|
|
|
} else {
|
|
|
|
destination[key] = [sourceValue copy];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (nestedDestination) {
|
|
|
|
RCTMergeRecursive(nestedDestination, sourceValue);
|
|
|
|
destination[key] = nestedDestination;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
destination[key] = sourceValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-11 19:06:03 +00:00
|
|
|
#pragma mark - RCTAsyncLocalStorage
|
|
|
|
|
|
|
|
@implementation RCTAsyncLocalStorage
|
|
|
|
{
|
|
|
|
BOOL _haveSetup;
|
|
|
|
// The manifest is a dictionary of all keys with small values inlined. Null values indicate values that are stored
|
|
|
|
// 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 *_manifest;
|
|
|
|
NSString *_manifestPath;
|
|
|
|
NSString *_storageDirectory;
|
|
|
|
}
|
|
|
|
|
2015-04-08 12:42:43 +00:00
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
|
2015-04-18 17:43:20 +00:00
|
|
|
- (dispatch_queue_t)methodQueue
|
|
|
|
{
|
|
|
|
return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
|
|
|
|
}
|
|
|
|
|
2015-03-11 19:06:03 +00:00
|
|
|
- (NSString *)_filePathForKey:(NSString *)key
|
|
|
|
{
|
|
|
|
NSString *safeFileName = RCTMD5Hash(key);
|
|
|
|
return [_storageDirectory stringByAppendingPathComponent:safeFileName];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (id)_ensureSetup
|
|
|
|
{
|
|
|
|
if (_haveSetup) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
NSString *documentDirectory =
|
|
|
|
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
|
|
NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES];
|
|
|
|
_storageDirectory = [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path];
|
|
|
|
NSError *error;
|
|
|
|
[[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory
|
|
|
|
withIntermediateDirectories:YES
|
|
|
|
attributes:nil
|
|
|
|
error:&error];
|
|
|
|
if (error) {
|
|
|
|
return RCTMakeError(@"Failed to create storage directory.", error, nil);
|
|
|
|
}
|
|
|
|
_manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename];
|
|
|
|
NSDictionary *errorOut;
|
|
|
|
NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut);
|
|
|
|
_manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new];
|
|
|
|
if (error) {
|
|
|
|
RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error);
|
|
|
|
_manifest = [NSMutableDictionary new];
|
|
|
|
}
|
|
|
|
_haveSetup = YES;
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (id)_writeManifest:(NSMutableArray **)errors
|
|
|
|
{
|
|
|
|
NSError *error;
|
|
|
|
NSString *serialized = RCTJSONStringify(_manifest, &error);
|
|
|
|
[serialized writeToFile:_manifestPath atomically:YES encoding:NSUTF8StringEncoding error:&error];
|
|
|
|
id errorOut;
|
|
|
|
if (error) {
|
|
|
|
errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil);
|
|
|
|
RCTAppendError(errorOut, errors);
|
|
|
|
}
|
|
|
|
return errorOut;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result
|
|
|
|
{
|
|
|
|
id errorOut = RCTErrorForKey(key);
|
|
|
|
if (errorOut) {
|
|
|
|
return errorOut;
|
|
|
|
}
|
2015-06-03 23:57:08 +00:00
|
|
|
id value = [self _getValueForKey:key errorOut:&errorOut];
|
|
|
|
[result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure.
|
|
|
|
return errorOut;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut
|
|
|
|
{
|
2015-03-11 19:06:03 +00:00
|
|
|
id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value.
|
|
|
|
if (value == [NSNull null]) {
|
|
|
|
NSString *filePath = [self _filePathForKey:key];
|
2015-06-03 23:57:08 +00:00
|
|
|
value = RCTReadFile(filePath, key, errorOut);
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
2015-06-03 23:57:08 +00:00
|
|
|
return value;
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (id)_writeEntry:(NSArray *)entry
|
|
|
|
{
|
|
|
|
if (![entry isKindOfClass:[NSArray class]] || entry.count != 2) {
|
|
|
|
return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil);
|
|
|
|
}
|
|
|
|
if (![entry[1] isKindOfClass:[NSString class]]) {
|
2015-03-31 21:46:15 +00:00
|
|
|
return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], @{@"key": entry[0]});
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
|
|
|
NSString *key = entry[0];
|
|
|
|
id errorOut = RCTErrorForKey(key);
|
|
|
|
if (errorOut) {
|
|
|
|
return errorOut;
|
|
|
|
}
|
|
|
|
NSString *value = entry[1];
|
|
|
|
NSString *filePath = [self _filePathForKey:key];
|
|
|
|
NSError *error;
|
|
|
|
if (value.length <= kInlineValueThreshold) {
|
|
|
|
if (_manifest[key] && _manifest[key] != [NSNull null]) {
|
|
|
|
// If the value already existed but wasn't inlined, remove the old file.
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
|
|
|
|
}
|
|
|
|
_manifest[key] = value;
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
[value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
|
|
|
|
if (error) {
|
|
|
|
errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key});
|
|
|
|
} else {
|
|
|
|
_manifest[key] = [NSNull null]; // Mark existence of file with null, any other value is inline data.
|
|
|
|
}
|
|
|
|
return errorOut;
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Exported JS Functions
|
|
|
|
|
2015-04-08 15:52:48 +00:00
|
|
|
RCT_EXPORT_METHOD(multiGet:(NSArray *)keys
|
|
|
|
callback:(RCTResponseSenderBlock)callback)
|
2015-03-11 19:06:03 +00:00
|
|
|
{
|
|
|
|
if (!callback) {
|
|
|
|
RCTLogError(@"Called getItem without a callback.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-04-18 17:43:20 +00:00
|
|
|
id errorOut = [self _ensureSetup];
|
|
|
|
if (errorOut) {
|
|
|
|
callback(@[@[errorOut], [NSNull null]]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
NSMutableArray *errors;
|
|
|
|
NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count];
|
|
|
|
for (NSString *key in keys) {
|
|
|
|
id keyError = [self _appendItemForKey:key toArray:result];
|
|
|
|
RCTAppendError(keyError, &errors);
|
|
|
|
}
|
|
|
|
callback(@[errors ?: [NSNull null], result]);
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
|
|
|
|
2015-04-08 15:52:48 +00:00
|
|
|
RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs
|
|
|
|
callback:(RCTResponseSenderBlock)callback)
|
2015-03-11 19:06:03 +00:00
|
|
|
{
|
2015-04-18 17:43:20 +00:00
|
|
|
id errorOut = [self _ensureSetup];
|
|
|
|
if (errorOut) {
|
|
|
|
callback(@[@[errorOut]]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
NSMutableArray *errors;
|
|
|
|
for (NSArray *entry in kvPairs) {
|
|
|
|
id keyError = [self _writeEntry:entry];
|
|
|
|
RCTAppendError(keyError, &errors);
|
|
|
|
}
|
|
|
|
[self _writeManifest:&errors];
|
|
|
|
if (callback) {
|
|
|
|
callback(@[errors ?: [NSNull null]]);
|
|
|
|
}
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
|
|
|
|
2015-06-03 23:57:08 +00:00
|
|
|
RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs
|
|
|
|
callback:(RCTResponseSenderBlock)callback)
|
|
|
|
{
|
|
|
|
id errorOut = [self _ensureSetup];
|
|
|
|
if (errorOut) {
|
|
|
|
callback(@[@[errorOut]]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
NSMutableArray *errors;
|
|
|
|
for (__strong NSArray *entry in kvPairs) {
|
|
|
|
id keyError;
|
|
|
|
NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError];
|
|
|
|
if (keyError) {
|
|
|
|
RCTAppendError(keyError, &errors);
|
|
|
|
} else {
|
|
|
|
if (value) {
|
|
|
|
NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy];
|
|
|
|
RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError));
|
|
|
|
entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)];
|
|
|
|
}
|
|
|
|
if (!keyError) {
|
|
|
|
keyError = [self _writeEntry:entry];
|
|
|
|
}
|
|
|
|
RCTAppendError(keyError, &errors);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
[self _writeManifest:&errors];
|
|
|
|
if (callback) {
|
|
|
|
callback(@[errors ?: [NSNull null]]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-08 15:52:48 +00:00
|
|
|
RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys
|
|
|
|
callback:(RCTResponseSenderBlock)callback)
|
2015-03-11 19:06:03 +00:00
|
|
|
{
|
2015-04-18 17:43:20 +00:00
|
|
|
id errorOut = [self _ensureSetup];
|
|
|
|
if (errorOut) {
|
|
|
|
callback(@[@[errorOut]]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
NSMutableArray *errors;
|
|
|
|
for (NSString *key in keys) {
|
|
|
|
id keyError = RCTErrorForKey(key);
|
|
|
|
if (!keyError) {
|
|
|
|
NSString *filePath = [self _filePathForKey:key];
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
|
|
|
|
[_manifest removeObjectForKey:key];
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
2015-04-18 17:43:20 +00:00
|
|
|
RCTAppendError(keyError, &errors);
|
|
|
|
}
|
|
|
|
[self _writeManifest:&errors];
|
|
|
|
if (callback) {
|
|
|
|
callback(@[errors ?: [NSNull null]]);
|
|
|
|
}
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
|
|
|
|
2015-04-08 15:52:48 +00:00
|
|
|
RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback)
|
2015-03-11 19:06:03 +00:00
|
|
|
{
|
2015-04-18 17:43:20 +00:00
|
|
|
id errorOut = [self _ensureSetup];
|
|
|
|
if (!errorOut) {
|
|
|
|
NSError *error;
|
|
|
|
for (NSString *key in _manifest) {
|
|
|
|
NSString *filePath = [self _filePathForKey:key];
|
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
2015-04-18 17:43:20 +00:00
|
|
|
[_manifest removeAllObjects];
|
|
|
|
errorOut = [self _writeManifest:nil];
|
|
|
|
}
|
|
|
|
if (callback) {
|
|
|
|
callback(@[errorOut ?: [NSNull null]]);
|
|
|
|
}
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
|
|
|
|
2015-04-08 15:52:48 +00:00
|
|
|
RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback)
|
2015-03-11 19:06:03 +00:00
|
|
|
{
|
2015-04-18 17:43:20 +00:00
|
|
|
id errorOut = [self _ensureSetup];
|
|
|
|
if (errorOut) {
|
|
|
|
callback(@[errorOut, [NSNull null]]);
|
|
|
|
} else {
|
|
|
|
callback(@[[NSNull null], [_manifest allKeys]]);
|
|
|
|
}
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@end
|