简体   繁体   中英

Can I access the files used for external binary storage in Core Data?

I am working on a media DB application. I have a custom model with data storage and think about rewriting it to Core Data. One use case that is of particular interest to me is movie storage. I store movie files in the DB, but the media framework can only read movies from files (not data).

Core Data offers a handy feature called “external binary storage”, where the entity data is not stored in the DB, but in an external file. This is transparent to the Core Data API user. My question is, can I get the path to the external file, so that I could store a movie using Core Data and then easily load it from its Core Data external file?

Yes, you CAN access the files stored in External Storage. It takes a bit of hacking, and may not be completely kosher with Apple's App Store, but you can do it fairly easily.

Assuming we have an NSManagedObject Subclass 'Media', with a 'data' property that has been set to 'Allows External Storage' in the Core Data Editor:

//  Media.h
//  Examples
//
//  Created by Garrett Shearer on 11/21/12.
//  Copyright (c) 2012 Garrett Shearer. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>


@interface CRMMedia : NSManagedObject

@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSData * data;

@end

And a handy-dandy NSString category:

//  NSString+Parse.m
//  Examples
//
//  Created by Garrett Shearer on 11/21/12.
//  Copyright (c) 2012 Garrett Shearer. All rights reserved.
//

#import "NSString+Parse.h"

@implementation NSString (Parse)

- (NSString*)returnBetweenString:(NSString *)inString1
                       andString:(NSString *)inString2
{
    NSRange substringRange = [self rangeBetweenString:inString1
                                            andString:inString2];
    logger(@"substringRange: (%d, %d)",substringRange.location,substringRange.length);
    logger(@"string (self): %@",self);
    return [self substringWithRange:substringRange];
}


/*
 Return the range of a substring, searching between a starting and ending delimeters
 Original Source: <http://cocoa.karelia.com/Foundation_Categories/NSString/Return_the_range_of.m>
 (See copyright notice at <http://cocoa.karelia.com>)
 */

/*" Find a string between the two given strings with the default options; the delimeter strings are not included in the result.
 "*/

- (NSRange) rangeBetweenString:(NSString *)inString1 andString:(NSString *)inString2
{
    return [self rangeBetweenString:inString1 andString:inString2 options:0];
}

/*" Find a string between the two given strings with the given options inMask; the delimeter strings are not included in the result.  The inMask parameter is the same as is passed to [NSString rangeOfString:options:range:].
 "*/

- (NSRange) rangeBetweenString:(NSString *)inString1 andString:(NSString *)inString2
                       options:(unsigned)inMask
{
    return [self rangeBetweenString:inString1 andString:inString2
                            options:inMask
                              range:NSMakeRange(0,[self length])];
}

/*" Find a string between the two given strings with the given options inMask and the given substring range inSearchRange; the delimeter strings are not included in the result.  The inMask parameter is the same as is passed to [NSString rangeOfString:options:range:].
 "*/

- (NSRange) rangeBetweenString:(NSString *)inString1 andString:(NSString *)inString2
                       options:(unsigned)inMask range:(NSRange)inSearchRange
{
    NSRange result;
    unsigned int foundLocation = inSearchRange.location;    // if no start string, start here
    NSRange stringEnd = NSMakeRange(NSMaxRange(inSearchRange),0); // if no end string, end here
    NSRange endSearchRange;
    if (nil != inString1)
    {
        // Find the range of the list start
        NSRange stringStart = [self rangeOfString:inString1 options:inMask range:inSearchRange];
        if (NSNotFound == stringStart.location)
        {
            return stringStart; // not found
        }
        foundLocation = NSMaxRange(stringStart);
    }
    endSearchRange = NSMakeRange( foundLocation, NSMaxRange(inSearchRange) - foundLocation );
    if (nil != inString2)
    {
        stringEnd = [self rangeOfString:inString2 options:inMask range:endSearchRange];
        if (NSNotFound == stringEnd.location)
        {
            return stringEnd;   // not found
        }
    }
    result = NSMakeRange( foundLocation, stringEnd.location - foundLocation );
    return result;
}


@end

Now its time for some magic.... We are going to create a Category method that parses the filename from the [data description] string. When operating on an instance of the Media subclass, 'data' is actually an 'External Storage Reference', not an NSData object. The filename of the actual data is stored in the description string.

//  Media+ExternalData.m
//  Examples
//
//  Created by Garrett Shearer on 11/21/12.
//  Copyright (c) 2012 Garrett Shearer. All rights reserved.
//

#import "Media+ExternalData.h"
#import "NSString+Parse.h"

@implementation Media (ExternalData)

- (NSString*)filePathString
{
    // Parse out the filename
    NSString *description = [self.data description];
    NSString *filename = [description returnBetweenString:@"path = " andString:@" ;"];
    // Determine the name of the store
    NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
    NSPersistentStore *ps = [psc.persistentStores objectAtIndex:0];
    NSURL *storeURL = [psc URLForPersistentStore:ps];
    NSString *storeNameWithExt = [storeURL lastPathComponent];
    NSString *storeName = [storeNameWithExt stringByDeletingPathExtension];
    // Generate path to the 'external data' directory
    NSString *documentsPath = [[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
                                                                       inDomains:NSUserDomainMask] lastObject] path];
    NSString *pathComponentToExternalStorage = [NSString stringWithFormat:@".%@_SUPPORT/_EXTERNAL_DATA",storeName];
    NSString *pathToExternalStorage = [documentsPath stringByAppendingPathComponent:pathComponentToExternalStorage];
    // Generate path to the media file
    NSString *pathToMedia = [pathToExternalStorage stringByAppendingPathComponent:filename];
    logger(@"pathToMedia: %@",pathToMedia);
    return pathToMedia;
}

- (NSURL*)filePathUrl
{
    NSURL *urlToMedia = [NSURL fileURLWithPath:[self filePathString]];
    return urlToMedia;
}

@end

Now you have an NSString path and a NSURL path to the file. JOY!!!

Something to take note of, I have had issues loading movies with this method... but I also came up with a workaround. It appears that MPMoviePlayer will not access the files in this directory, so the solution was to temporarily copy the file to the documents directory, and play that. Then delete the temp copy when I unload my view:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self copyTmpFile];
}

- (void)viewDidUnload
{
    logger(@"viewDidUnload");
    [_moviePlayer stop];
    [_moviePlayer.view removeFromSuperview];
    [self cleanupTmpFile];
    [super viewDidUnload];
}

- (NSString*)tmpFilePath
{
    NSString *documentsPath = [[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
                                                                       inDomains:NSUserDomainMask] lastObject] path];
    NSString *tmpFilePath = [documentsPath stringByAppendingPathComponent:@"temp_video.m4v"];
    return tmpFilePath;
}

- (void)copyTmpFile
{
    NSString *tmpFilePath = [self tmpFilePath];
    NSFileManager *mgr = [NSFileManager defaultManager];
    NSError *err = nil;
    if([mgr fileExistsAtPath:tmpFilePath])
    {
        [mgr removeItemAtPath:tmpFilePath error:nil];
    }

    [mgr copyItemAtPath:_media.filePathString toPath:tmpFilePath error:&err];
    if(err)
    {
        logger(@"error: %@",err.description);
    }
}

- (void)cleanupTmpFile
{
    NSString *tmpFilePath = [self tmpFilePath];
    NSFileManager *mgr = [NSFileManager defaultManager];
    if([mgr fileExistsAtPath:tmpFilePath])
    {
        [mgr removeItemAtPath:tmpFilePath error:nil];
    }
}

Good Luck!

If you want to access the data directly (ie, not through CoreData), you may be better off giving each file a UUID as name, and store that name in the database, and store the actual file yourself.

If you use UIManagedDocument, you have several options. Using the above technique, you can store the files alongside the database, because UIManagedDocument is really a file package.

Alternatively, you can subclass from UIManagedDocument and override the methods that handle reading/writing the "extra" files. This will give you access to the files themselves. You can hook there to do whatever you want, including grabbing the actual URL to the file CoreData automatically creates.

- (id)additionalContentForURL:(NSURL *)absoluteURL error:(NSError **)error
- (BOOL)readAdditionalContentFromURL:(NSURL *)absoluteURL error:(NSError **)error
- (BOOL)writeAdditionalContent:(id)content toURL:(NSURL *)absoluteURL originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError **)error

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM