I'm iterating through the user's Dropbox content (using the loadedMetaData delegate) in order to get information about all files and folder names present in the user's dropbox (I need this so that I cab download all necessary data to the Documents folder of the app for offline use). The information shall be stored in an NSMutabledirectionary where "key = folder-name" and the object is always an array containing all files inside the folder. I'm doing this:
-(void)restClient:(DBRestClient *)client loadedMetadata:(DBMetadata *)metadata
{
if (metadata.isDirectory) {
//subfolders are loaded into array
for (DBMetadata *directory in metadata.contents) {
if (directory.isDirectory)
{
[directoryList addObject:directory.filename];
//
}
}
//files are loaded according loadmeta-folder (first run is root folder)
[fileList removeAllObjects];
for (DBMetadata *file in metadata.contents) {
if (!file.isDirectory)
{
NSLog(#"Directy is called %#", metadata.path);
//NSLog(#"%# was last changed %#", file.filename, file.lastModifiedDate);
[fileList addObject:file.filename];
[fileRevisionDates addObject:file.lastModifiedDate];
}
}
NSLog(#"Key is called %#", metadata.path);
//This is where I store the information in the dictionary
[subFolderContent setValue:fileList forKey:metadata.path];
//loadmetadata for all sub-folders
for (int i = 0; i < directoryList.count; i++) {
[restClient loadMetadata:[NSString stringWithFormat:#"/TestFolder/%#/", [directoryList objectAtIndex:i]]];
}
}
}
Now this basically works, but of course, every time the array "fileList" is updated, all values for each key in the dictionary are updated accordingly... What am I missing? Or is there a better way to achieve this?
Thanks for your help!
Tom
Here's some rough code that might work. (I haven't tested it at all.) To answer your direct question, note that I'm initializing a new fileList in this method instead of having a global one somewhere else. This is what the commenters above were getting at.
I also did a little cleanup:
There's no need to keep the list of directories in an array and then call loadMetadata on each later. You can just call loadMetadata on each directory as you see it.
I deleted fileRevisionDates since it didn't seem usable in its current state. (I guess it was literally an array of dates? You probably need an NSMutableDictionary instead.)
I fixed the path construction on the recursive call to loadMetadata. If a user's Dropbox contained a directory like /foo/bar/baz, it looked like your code would try to get metadata on a path like /TestFolder/baz. I think the change I made will take care of that.
Again, I haven't tested this code at all (and my Objective-C is not very good), so there may be bugs/typos. Others should feel free to suggest edits:
-(void)restClient:(DBRestClient *)client loadedMetadata:(DBMetadata *)metadata {
if (metadata.isDirectory) {
NSMutableArray *fileList = [[NSMutableArray alloc] init];
for (DBMetadata *entry in metadata.contents) {
if (entry.isDirectory) {
[restClient loadMetadata:[NSString stringWithFormat:#"%#/%#", metadata.path, entry.filename]];
}
else {
[fileList addObject:entry.filename];
}
}
[subFolderContent setValue:fileList forKey:metadata.path];
}
}
Finally, please note that this is not a great way to enumerate the contents of a user's Dropbox. See https://blogs.dropbox.com/developers/2013/12/efficiently-enumerating-dropbox-with-delta/ for the preferred approach, which is to use the /delta endpoint.
Related
I have a React Native application which uses React Native Video with iOS caching. I have been working on a method inside RCTVideoCache.m which would manually delete the data of a particular cache key. According to the documentation of SPTPersistentCache, which the video library uses for caching, data can be deleted either by locking/unlocking a file and invoking a wipe or after inspecting the source code of SPTPersistentCache.h with a method named removeDataForKeys.
I have tried both ways, however, unsuccessfully.
In my first try, I am using wipeLockedFiles. I have created a deleteFromCache() method inside RCTVideoCache.m. Since all my video files are unlocked by default, in this method I am trying to lock the file corresponding to my cacheKey and invoke a wipe on all locked files (which would consist of only my target cacheKey file) as it is demonstrated in the documentation. This method looks like:
- (void)deleteFromCache:(NSString *)cacheKey withCallback:(void(^)(BOOL))handler;
{
[self.videoCache lockDataForKeys:#[cacheKey] callback:nil queue:nil];
[self.videoCache wipeLockedFiles];
NSLog(#"Size = %#", #(self.videoCache.totalUsedSizeInBytes));
handler(YES);
}
The following results in two errors during compilation:
/Users/.../MyApp/node_modules/react-native-video/ios/VideoCaching/RCTVideoCache.m:79:20: error: no visible #interface for 'SPTPersistentCache' declares the selector 'lockDataForKeys:callback:queue:'
[self.videoCache lockDataForKeys:#[cacheKey] callback:nil queue:nil];
~~~~~~~~~~~~~~~ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/.../MyApp/node_modules/react-native-video/ios/VideoCaching/RCTVideoCache.m:80:20: error: no visible #interface for 'SPTPersistentCache' declares the selector 'wipeLockedFiles'
[self.videoCache wipeLockedFiles];
~~~~~~~~~~~~~~~ ^~~~~~~~~~~~~~~
I really have no idea why these selectors are not visible from SPTPersistentCache.
In my second try, I am using removeDataForKeys(). Again, I have created a deleteFromCache() method inside RCTVideoCache.m which looks like this:
- (void)deleteFromCache:(NSString *)cacheKey withCallback:(void(^)(BOOL))handler;
{
[self.videoCache removeDataForKeys:#[cacheKey] callback:^(SPTPersistentCacheResponse * _Nonnull response) {
NSLog(#"Result output: %#", response.output);
NSLog(#"Error output: %#", [response.error localizedDescription]);
} onQueue:dispatch_get_main_queue()];
NSLog(#"Size = %#", #(self.videoCache.totalUsedSizeInBytes));
handler(YES);
}
In this second way, there are no errors, however, the data of the key is never deleted. Also, both NSLogs for the response output null inside the terminal.
I am 100% sure that the cacheKey I am providing to my deleteFromCache() method is correct and data corresponding to it exists. However, in both methods NSLog(#"Size = %#", #(self.videoCache.totalUsedSizeInBytes)); does not change and I can also manually verify that the file has not been deleted.
I am really stuck and do not know what is wrong with the code I've written in both cases and why neither of them works. I would appreciate any help on this!
You can delete all sub-folder's files (tmp/rct.video.cache), iterating each one:
+ (void)deleteFromCache
{
NSArray* tmpDirectory = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.temporaryCachePath error:NULL];
for (NSString *file in tmpDirectory) {
[[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:#"%#%#", self.temporaryCachePath, file] error:NULL];
}
}
I ran your example and discovered that you are using incorrect method signatures. These methods simply don't exist in the caching library, their signatures are different.
Try something like this:
- (void)deleteFromCache:(NSString *)cacheKey withCallback:(void(^)(BOOL))handler;
{
NSLog(#"Size before = %#", #(self.videoCache.totalUsedSizeInBytes));
[self.videoCache lockDataForKeys:#[cacheKey] callback:nil onQueue:nil];
[self.videoCache wipeLockedFilesWithCallback:^(SPTPersistentCacheResponse * _Nonnull response) {
NSLog(#"Size after = %#, response = %#", #(self.videoCache.totalUsedSizeInBytes), response);
// Call handler after the files are wiped
handler(YES);
} onQueue:nil];
}
I have no idea why the second approach doesn't work, but NSLog(#"Size = %#", #(self.videoCache.totalUsedSizeInBytes)); is for sure called before the actual deletion happens. In the example I posted above, I have moved the logging statement into the callback closure, so that it reports the size before and after the deletion takes place.
I'm seeking advice on using iCloud in a limited way. The docs are focused on syncing between multiple devices, which I do not need. When I say iCloud, I'm also including iCloud Drive, if that matters.
My iPhone app stores its data and state in a single SQLite database file that I want to save to iCloud (or iCloud Drive) for the purpose of backup, not for syncing changes between multiple devices. At launch, SQLite opens the database file and uses it synchronously (I'm using FMDB). The database file should remain on the device so, if iCloud is down, SQLite won't know or care. iCloud would have a shadow copy.
The iCloud copy does not always need to be current -- I'm okay with programmatically initiating an update to iCloud, and the restore if necessary. If the local database file becomes corrupted or if the user deletes the app, I would restore the file from iCloud. The file would usually be smaller than 1 MB, but might be large (100 MB), but mostly invariant.
I'm not syncing because of the data integrity risks of sharing whole SQLite files, and I cannot convert to Core Data (it's a huge app using FMDB).
I know that the iOS does daily backups to iCloud, and that would be sufficient if I could restore just my app from iCloud (and not the entire device).
I need advice on using iCloud for a shadow backup and restore, but not syncing.
UPDATE: I have made progress on this issue; I can save and restore my real SQLite file to/from iCloud. I subclassed UIDocument with these two overrides:
#implementation MyDocument
// Override this method to return the document data to be saved.
- (id)contentsForType:(NSString *)typeName error:(NSError * _Nullable *)outError
{
NSString *databasePath = ... // path to my SQLite file in Documents;
NSData *dbData = [NSData dataWithContentsOfFile: databasePath];
return dbData; // the entire database file
}
// Override this method to load the document data into the app’s data model.
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError * _Nullable *)outError
{
NSString *databasePath = ... // path to my SQLite file in Documents;
NSData *dbData = contents;
BOOL ret = [dbData writeToFile: databasePath atomically:YES];
return YES;
}
#end
My SQLite database file is in Documents and it stays there, but I write the shadow copy to iCloud thusly:
- (void)sendToICloud
{
NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
NSString *iCloudFileName = #"..."; // a file name I choose
NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:#"Documents"]
URLByAppendingPathComponent:iCloudFileName];
MyDocument *myDoc = [[MyDocument alloc] initWithFileURL:ubiquitousPackage];
[myDoc saveToURL:[myDoc fileURL] forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if ( success ) {
[myDoc closeWithCompletionHandler:^(BOOL success) {
}];
} else {
NSLog(#"iCloud write Syncing FAILED with iCloud");
}
}];
}
Now my issues are:
I have created a custom container in Xcode that appears in my entitlements file and in the Dev portal for the App ID. I can save documents to iCloud from either of my two devices (iPhone and iPad). But, my two devices do not see each other's files. There are numerous old threads about this with random solutions, but nothing has solved the problem.
The iCloud documentation says files are updated incrementally, but I don't know if that applies in my case. If I use a new filename for MyDocument (my iCloudFileName, above), I realize that an entire new file will be sent to iCloud. But, if I reuse the previous filename and send an updated NSData each time (as I'm doing in loadFromContents:ofType:error:), will iOS only send the parts that have changed since the last time I saved the file?
Another subject line now would be: Using iCloud for file backup across multiple devices.
I've solved my primary issue of seeing iCloud data sent by multiple devices. According to WWDC 2015 "Building Document Based App," to see all files (even those from other devices), use NSMetadataQuery, not NSFileManager. Also, the video presentation includes a warning about using NSURL because documents might be moved (use Bookmarks), but that doesn't affect my use case.
I still don't know if sending partially-changed NSData will cause incremental updates.
My UIDocument overrides and my sendToICloud method remain the same. What's new is my iCloud search.
If you aren't writing to the default iCloud Documents directory, writing will fail if the directory does not exist.
It's easier to just create it every time, thusly:
- (void)writeToiCloud
{
// ...
#define UBIQUITY_ID #"iCloud.TEAMID.bundleID.appName"
NSFileManager *defaultManager = [NSFileManager defaultManager];
NSURL *ubiq = [defaultManager URLForUbiquityContainerIdentifier:UBIQUITY_ID];
NSURL *ubiquitousFolder = [ubiq URLByAppendingPathComponent:#"MyFolder"];
[defaultManager createDirectoryAtURL:ubiquitousFolder
withIntermediateDirectories:YES
attributes:nil error:nil];
// ...
// ... forSaveOperation:UIDocumentSaveForCreating ...
}
- (void)updateFileList
{
// Use an iVar so query stays in scope
// Instance variables maintain a strong reference to the objects by default
query = [[NSMetadataQuery alloc] init];
[query setSearchScopes:#[ // use both for testing, then only DocumentsScope
NSMetadataQueryUbiquitousDataScope, // not in Documents
NSMetadataQueryUbiquitousDocumentsScope, // in Documents
]
];
// add a predicate if desired to restrict the search.
// No predicate finds everything, but Apple says:
// a query can’t be started if ... no predicate has been specified.
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K like '*'", NSMetadataItemFSNameKey];
// NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K CONTAINS %#", NSMetadataItemPathKey, #"/MyFolder/"];
// NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K CONTAINS %#", NSMetadataItemPathKey, #"/Documents/"];
[query setPredicate:predicate];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(queryDidFinish:)
name:NSMetadataQueryDidFinishGatheringNotification
object:query];
[query startQuery];
}
- (void)queryDidFinish:(NSNotification *)notification
{
// iVar: NSMetadataQuery *query
for ( NSMetadataItem *item in [query results] ) {
NSURL *itemURL = [item valueForAttribute:NSMetadataItemURLKey];
NSLog(#"fileName: %#",itemURL.lastPathComponent);
// can't use getResourceValue:forKey:error
// that applies to URLs that represent file system resources.
if ( [itemURL.absoluteString hasSuffix:#"/"] ) {
continue; // skip directories
}
// process the itemURL here ...
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSMetadataQueryDidFinishGatheringNotification
object:query];
}
I'm using Nico Kreipke's FTPManager (click here to go to GiHub) to download some data from an FTP address.
The code works if it's run before the user's first interaction, after that it will usually fail (about 9 out of 10).
When it fails, the following message is written (0x_ are actually valid addresses):
request (0x_) other than the current request(0x0) signalled it was complete on connection 0x_
That message isn't written by neither my code nor by FTPManager, but by Apple's. On its GitHub, I've found some one with the same error, but the source of it could possible be the same as mine. (That person wasn't using ARC.)
If I try to print the objects of those addresses with the pocommand, the console writes that there's no description available.
Also, the memory keeps adding up until the app receives a memory warning, and soon after the OS terminates it.
By pausing the app when that message appears, I can see that the main thread is in a run loop.
CFRunLoopRun();
The Code
self.ftpManager = [[FTPManager alloc] init];
[self downloadFTPFiles:#"192.168.2.1/sda1/1668"];
ftpManageris a strong reference.
The downloadFTPFiles: method:
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer* server = [FMServer serverWithDestination: basePath username:#"test" password:#"test"];
NSArray* serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary * sDataI = serverData[i];
NSString* name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber* type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString * nextDestination = [basePath stringByAppendingPathComponent: name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
[self.ftpManager downloadFile:name toDirectory:[NSURL fileURLWithPath:NSHomeDirectory()] fromServer:server];
}
}
}
What I've Done
I've tried running that code on several places:
The app delegate's application:didFinishLaunchingWithOptions:;
The viewDidLoad, viewWillAppear: and viewDidAppear: of the a view controller loaded just after the app launches and a view controller presented later.
By an action triggered with a button event.
The download of the data is always well performed when executed by the delegate or a view controller loaded with the app (with an exception). But when run after the user's first interaction with the app, it'll most likely fail with the mentioned error.
The exception for view controllers loaded before the user's first interaction is when the call is in either the viewWillAppear: or viewDidAppear: methods. When it's called a second time (for example, a tab of a tab bar controller) it'll also, most likely, fail.
The Question
Does anyone have an idea of what may be happening, or if I'm doing something wrong? Or any alternative solution, maybe?
Any help to solve this problem will be welcomed.
Thanks,
Tiago
I ended up sending the downloadFile:toDirectory:fromServer: message inside a dispatch_async block. I've also created an FTPManage for every file downloaded.
It worked, but I have no idea why.
I'm leaving this answer to whomever crosses with this problem.
If anyone can let me know why this technique worked, please comment bellow so I can update the answer.
Here's the new way I'm downloading each file:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
Again, If you know why this worked, let me know.
Thanks.
Full Method
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer *server = [FMServer serverWithDestination:basePath username:#"test" password:#"test"];
NSArray *serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary *sDataI = serverData[i];
NSString *name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber *type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString *nextDestination = [basePath stringByAppendingPathComponent:name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
}
}
}
Id like to zip a folder using ZipKit??
I cant seem to locate a good documentation for the usage of functions in the ZipKit library.
Can some one explain the method for folder zipping?
ZKFileArchive *archive = [ZKFileArchive archiveWithArchivePath:filePath];
[archive deflateDirectory:param1 relativeToPath:param2 usingResourceFork:NO];
What needs to be passed in param1 and param2??I dont understand the function call here?
It would be great if some one could post an example for it?
Thank you!
Looking at the answer in this related question, here's a good example to work with.
Your param1 is the folder (with it's path) to be archived, and the relative path could be the parent folder.
NSString *zipFilePath = #"/Documents/zipped.zip";
ZKFileArchive *archive = [ZKFileArchive archiveWithArchivePath:zipFilePath];
NSInteger result = [archive deflateDirectory:#"/Documents/myfolder" relativeToPath:#"/Documents" usingResourceFork:NO];
It would be nice if ZipKit had better documentation than the limited info it has.
I created the following category method for NSFileManager using ZipKit.
- (BOOL)zipContentsOfDirectoryAtPath:(NSString *)directory toPath:(NSString *)filename recursive:(BOOL)recursive {
// If there is already a file at the destination, delete it
if ([self fileExistsAtPath:filename]) {
[self removeItemAtPath:filename error:nil];
}
#try {
ZKFileArchive *archive = [ZKFileArchive archiveWithArchivePath:filename];
NSInteger result = [archive deflateDirectory:directory relativeToPath:directory usingResourceFork:NO];
return result == zkSucceeded;
}
#catch (NSException *exception) {
if ([self fileExistsAtPath:filename]) {
[self removeItemAtPath:filename error:nil];
}
}
return NO;
}
The directory parameter is a path to the directory (and its contents) that you wish to the zip up. The filename parameter is a path to the resulting zip file you want as a result.
I'm having a lot of trouble deciphering Apple's documentation around UIManagedDocument, specifically the following methods:
- (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
Has anyone successfully managed to save additional content into the "addition content" directory inside their UIManagedDocument packages? I'm looking to save straight images (PNGs, JPEGs, etc) and videos (m4v, etc) into this directory using UUIDs as the filenames (with the correct file extension), and storing references to these individual files as NSString file paths within my persistent store.
Credit goes to Apple DTS for helping me understand this class. I'm sharing some of the example they helped me with here (modified slightly).
OK, so basically it works like this: subclass UIManagedDocument, and implement the following methods (where the extraInfo property is just an NSDictionary implemented on our subclass):
- (BOOL)readAdditionalContentFromURL:(NSURL *)absoluteURL error:(NSError **)error
{
NSURL *myURL = [absoluteURL URLByAppendingPathComponent:#"AdditionalInformation.plist"];
self.extraInfo = [NSDictionary dictionaryWithContentsOfURL:myURL];
return YES;
}
- (id)additionalContentForURL:(NSURL *)absoluteURL error:(NSError **)error
{
if (!self.extraInfo) {
return [NSDictionary dictionaryWithObjectsAndKeys: #"Picard", #"Captain", [[NSDate date] description], #"RightNow", nil];
} else {
NSMutableDictionary *updatedFriendInfo = [self.extraInfo mutableCopy];
[updatedFriendInfo setObject:[[NSDate date] description] forKey:#"RightNow"];
[updatedFriendInfo setObject:#"YES" forKey:#"Updated"];
return updatedFriendInfo;
}
}
- (BOOL)writeAdditionalContent:(id)content toURL:(NSURL *)absoluteURL originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError **)error
{
if (content) {
NSURL *myURL = [absoluteURL URLByAppendingPathComponent:#"AdditionalInformation.plist"];
[(NSDictionary *)content writeToURL:myURL atomically:NO];
}
return YES;
}
UIManagedDocument will call these methods when it needs to, automatically saving whatever you need to save to the document package inside an AdditionalContent directory.
If you need to force a save, simply call the following on your UIManagedDocument instance:
[self updateChangeCount:UIDocumentChangeDone];
At present, I'm not using this for images and videos — but the example should give you enough to go off.
The documentation for -additionalContentForURL:error: indicates that returning a nil supposed to signal an error.
A return value of nil indicates an error condition. To avoid generating
an exception, you must return a value from this method. If it is not always
the case that there will be additional content, you should return a sentinel value (for example, an NSNull instance) that you check for in
writeAdditionalContent:toURL:originalContentsURL:error:.
I override -writeContents:andAttributes:safelyToURL:forSaveOperation:error: for another purpose (doing some stuff on first save of a new document), and calling super invokes the NSException gods because contents value is nil, not an NSDictionary as seemingly expected by UIManagedDocument. Hmm.
The more you know...
P.S. I guess it depends on the time of day with -writeContents:andAttributes:... It once threw an exception complaining about expecting an NSDictionary, but later threw an exception complaining that I didn't pass it an NSData. My eyebrow could not be raised in a more Spock-like fashion than it is right now.