Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 826
zoul Avatar asked Apr 19 '12 06:04

zoul


People also ask

How can you retrieve data from Core Data?

Fetching Data From CoreData We have created a function fetch() whose return type is array of College(Entity). For fetching the data we just write context. fetch and pass fetchRequest that will generate an exception so we handle it by writing try catch, so we fetched our all the data from CoreData.

Where does Core Data store data?

The persistent store should be located in the AppData > Library > Application Support directory.

What should Core Data be used for?

Use Core Data to save your application's permanent data for offline use, to cache temporary data, and to add undo functionality to your app on a single device. To sync data across multiple devices in a single iCloud account, Core Data automatically mirrors your schema to a CloudKit container.

What database does Core Data use?

Even though Core Data knows how to use a SQLite database as its persistent store, that doesn't mean you can hand it any SQLite database. The database schema of the SQLite database used by Core Data is an implementation detail of the framework. It isn't publicly documented and liable to change.


2 Answers

If you want to access the data directly (i.e., 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
like image 22
Jody Hagins Avatar answered Oct 24 '22 07:10

Jody Hagins


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!

like image 171
G. Shearer Avatar answered Oct 24 '22 08:10

G. Shearer