Save files avoiding duplicate filenames and overwriting - objective-c

So, here's my situation :
I've got an application prompting the user to select a folder to save his files
Now let's say his files are : a.png, b.png, c.png
What I want is :
if file - let's say - a.png already exists in the selected folder, DON'T overwrite it. Instead create a file with name a (1).png. If a file a (1).png already exists, then name it like a (2).png, and so on - pretty much like OS X does when copy-pasting files with the same name.
This is how I'm currently doing it - but it still doesn't strike me ok :
NSString* target = [self getTargetPathForFile:filename path:folder];
NSString* fname = [target lastPathComponent];
while ([[NSFileManager defaultManager] fileExistsAtPath: [folder stringByAppendingPathComponent:fname]])
{
//NSLog(#"FNAME : %#",fname);
if ([self str:fname containsString:#")." options:nil])
{
NSArray* a = [fname componentsSeparatedByString:#"("];
NSArray* b = [[a objectAtIndex:1] componentsSeparatedByString:#")"];
fname = [NSString stringWithFormat:#"%#(%d)%#",[a objectAtIndex:0],[[b objectAtIndex:0] intValue]+1,[b objectAtIndex:1]];
}
else
{
fname = [NSString stringWithFormat:#"%# (1).%#",[fname stringByDeletingPathExtension],[fname pathExtension]];
}
//NSLog(#"Setting filename to :: %#",fname);
}
target = [folder stringByAppendingPathComponent:fname];
Any ideas how to go about this? Is there any Cocoa-friendly (or built-in) method for this?

Why not try this one :
NSString* fname = [target lastPathComponent];
NSString* fnameNoExt = [fname stringByDeletingPathExtension];
NSString* extension = [fname pathExtension];
int fileIndex = 1;
while ([[NSFileManager defaultManager] fileExistsAtPath: [folder stringByAppendingPathComponent:fname]])
{
//NSLog(#"FNAME : %#",fname);
fname = [NSString stringWithFormat:#"%# (%d).%#", fnameNoExt, fileIndex, extension];
//NSLog(#"Setting filename to :: %#",fname);
fileIndex++;
}
target = [folder stringByAppendingPathComponent:fname];

If you don't really care about the file names as you explained in one of your comment, and can consider using automatically-generated file names instead, use either CFUUID, NSProcessInfo, or mkstemp.
Using CFUUID to generate a unique identifier str:
CFUUIDRef uuidref = CFUUIDCreate(NULL);
CFStringRef cfstr = CFUUIDCreateString(NULL, uuidref);
// If using ARC:
NSString* filename = (__bridge_transfer NSString*)cfstr;
// If not using ARC
NSString* filename = [(NSString*)cfstr autorelease];
// Then release the CFUUIDRef
CFRelease(uuidref);
Using NSProcessInfo
NSString* filename = [[NSProcessInfo processInfo] globallyUniqueString];
Using mkstemp: see CocoaWithLove

Related

Read from file.Plist Returns Null

Program that Creates multiple Plist's Paths for Different information.
But only one path is not working.
(i think "writeToFile" is the problem)
code:
-(NSString *) createPath:(NSString *)withFileName
{
NSArray *paths =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,
YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:withFileName];
return path;
}
Path
NSLog = /var/mobile/Applications/02CABC0A-6B5B-4097-A9D1-4336BE8230B7/Documents/MessagesDB.plist
&
-(void) messagesDbFlush
{
// save it to the file for persistency
NSString *messagesDB_Path = [self createPath:_fileMessagesDB];
[_messagesDB writeToFile:messagesDB_Path atomically:YES];
NSMutableArray *ReturnsInfo = [[NSMutableArray alloc ]initWithContentsOfFile:messagesDB_Path];
NSLog(#"ReturnsInfo is : %#", ReturnsInfo);
}
"ReturnsInfo" Array is Null :/
Anyone please help?
I once had the same error.
1) Check the name of the plist in the directory listing to match your coded one
2) Check Project settings, manually delete the pre-existing plist from the "Build Settings" > "Copy Bundle Resources", and drag drop from the list.
3) Select the plist in directory listing, check Utilities sidebar, check Identity & Type > Location as valid
4) If you deleted the app's "default" plist aka bundle identifier, add copy build phase, choose destination, choose pref folder as absolut path check "copy only when installing"
This solved my returning null.
And if all fails on the bundle identifier, you can always copy the plist to pref folder by code:
NSString *path = [#"~/Library/Preferences/com.MyCompany.MyApp.plist" stringByExpandingTildeInPath];
BOOL PrefsExist=[[NSFileManager defaultManager] fileExistsAtPath:path];
NSString *copyPrefsPath = [#"~/Library/Preferences/com.MyCompany.MyApp.plist" stringByExpandingTildeInPath];
NSFileManager *fileManager = [NSFileManager defaultManager];
if (PrefsExist == 0)
{
// Copy the plist to prefs folder (one-time event)
NSString *tessdataPath = [[NSBundle mainBundle] pathForResource:#"com.MyCompany.MyApp" ofType:#"plist"];
[fileManager copyItemAtPath:tessdataPath toPath:path error:&error];
} else
{
// Read/Write the values from plist
}
i have stored following array with 5 objects in it, its working fine on my side, try it
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:5];
for(int i=0;i<5;i++)
[array addObject:#"This is Demo String, You can write your own String here"];
NSString *_fileMessagesDB = #"MessagesDB.plist";
// save it to the file for persistency
NSString *messagesDB_Path = [self createPath:_fileMessagesDB];
[array writeToFile:messagesDB_Path atomically:YES];
NSMutableArray *ReturnsInfo = [[NSMutableArray alloc ]initWithContentsOfFile:messagesDB_Path];
NSLog(#"ReturnsInfo is : %#", ReturnsInfo);

Cannot append to NSString

I'm trying to append the file extension to a the stringValue returned by a subclassed NSTextFieldCell
I've tried everything I knew and could find on the internet, but this is just giving me a headache
the method is the following:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSMutableString *filename = [[NSMutableString alloc] init];
[filename appendString:self.stringValue];
NSString *iconFileName = [[[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:filename] stringByAppendingPathExtension:#"png"];
NSLog(#"%#", iconFileName);
}
The returned value is without the extension though!
I've also tried the following:
filename = [NSString stringWithFormat: #"%#.png", filename];
This returns the "filename" string without the ".png"
Similarly:
filename = [filename stringByAppendingString: #".png"];
returns just the "filename"
The table column where this cell belongs to is bound to an NSObject, and the method that sends the data to the column is the following:
- (NSString *) nationString {
NSMutableString *string = [[NSMutableString alloc] init];
int index = 0;
if (nationAddress && nationAddress > 0x0) {
index = [[[[controller database] nationsAddressIndex] valueForKey:[NSString stringWithFormat:#"%lli", nationAddress]] intValue];
Nation *nationality = [[[controller database] nations] objectAtIndex:index];
[string appendString:[nationality name]];
}
else {
[string appendString:#"---"];
}
return string;
}
Anyone has any idea why this might be happening, or can suggest any alternatives?
Any help will be appreciated
Thanks
This should return the complete path with extension:
NSString *path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:#"Default.png"];
NSLog(#"%#", path);
So, assuming self.stringValue includes the extension, your method should work with this:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSString *iconFileName = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:self.stringValue];
NSLog(#"%#", iconFileName);
}
If it doesn't include the extension, try this:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSString *strWithPath = [NSString stringWithFormat:#"%#.png", self.stringValue];
NSString *iconFileName = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:strWithPath];
NSLog(#"%#", iconFileName);
}
Just for test. Try to use this code and update here a output values:
NSMutableString *filename = [[NSMutableString alloc] init];
[filename appendString:self.stringValue];
NSLog(#"text1: %# ;", filename);
filename = [NSString stringWithFormat: #"%#.png", filename];
filename = [NSString stringWithFormat: #"%#.png%#", filename, filename];
NSLog(#"text2: %# ;", filename);
These should work (barring a typo):
NSString* filename = #"abc";
NSString* result1 = [NSString stringWithFormat:#"%#.png", filename];
NSString* result2 = [filename stringByAppendingString:#".png"];
NSMutableString* result3 = [filename mutableCopy];
[result3 appendString:#".png"];
If they don't appear to be working then you have some problem with how you're initializing or displaying your values.
Hint: Place an NSLog(#"The answer is %#", resultN); statement immediately after each of the above (with "resultN" changed appropriately) to see what you're getting. Keep in mind that if you look from a different object you may be looking at different variables.

NSString unique file path to avoid name collisions

Is there a simple way to take a given file path and modify it in order to avoid name collisions? Something like:
[StringUtils stringToAvoidNameCollisionForPath:path];
that for a given path of type: /foo/bar/file.png, will return /foo/bar/file-1.png and later it will increment that "-1" similarly to what Safari does for downloaded files.
UPDATE:
I followed Ash Furrow's suggestion and I posted my implementation as answer :)
I had a similar problem, and came up with a slightly broader approach, that attempts to name files the same way iTunes would (when you have it set to manage your library and you have multiple tracks with the same name, etc.)
It works in a loop, so the function can be called multiple times and still produce valid output. Explaining the arguments, fileName is the name of the file with no path or extension (e.g. "file"), folder is just the path (e.g. "/foo/bar"), and fileType is just the extension (e.g. "png"). These three could be passed in as one string and be split out after, but in my case it made sense to separate them.
currentPath (which can be empty, but not nil), is useful when you're renaming a file, not creating a new one. For example, if you have "/foo/bar/file 1.png" that you're trying to rename to "/foo/bar/file.png", you would pass in "/foo/bar/file 1.png" for currentPath, and if "/foo/bar/file.png" already exists, you'll get back the path you started with, instead of seeing that "/foo/bar/file 1.png" and returning "/foo/bar/file 2.png"
+ (NSString *)uniqueFile:(NSString *)fileName
inFolder:(NSString *)folder
withExtension:(NSString *)fileType
mayDuplicatePath:(NSString *)currentPath
{
NSUInteger existingCount = 0;
NSString *result;
NSFileManager *manager = [NSFileManager defaultManager];
do {
NSString *format = existingCount > 0 ? #"%# %lu" : #"%#";
fileName = [NSString stringWithFormat:format, fileName, existingCount++];
result = [fileName stringByAppendingFormat:#".%#", [fileType lowercaseString]];
result = [folder stringByAppendingPathComponent:result];
} while ([manager fileExistsAtPath:result] &&
// This comparison must be case insensitive, as the file system is most likely so
[result caseInsensitiveCompare:currentPath] != NSOrderedSame);
return result;
}
I decided to implement my own solution and I want to share my code. It's not the most desirable implementation, but it seems to do the job:
+ (NSString *)stringToAvoidNameCollisionForPath:(NSString *)path {
// raise an exception for invalid paths
if (path == nil || [path length] == 0) {
[NSException raise:#"DMStringUtilsException" format:#"Invalid path"];
}
NSFileManager *manager = [[[NSFileManager alloc] init] autorelease];
BOOL isDirectory;
// file does not exist, so the path doesn't need to change
if (![manager fileExistsAtPath:path isDirectory:&isDirectory]) {
return path;
}
NSString *lastComponent = [path lastPathComponent];
NSString *fileName = isDirectory ? lastComponent : [lastComponent stringByDeletingPathExtension];
NSString *ext = isDirectory ? #"" : [NSString stringWithFormat:#".%#", [path pathExtension]];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"-([0-9]{1,})$" options:0 error:nil];
NSArray *matches = [regex matchesInString:fileName options:0 range:STRING_RANGE(fileName)];
// missing suffix... start from 1 (foo-1.ext)
if ([matches count] == 0) {
return [NSString stringWithFormat:#"%#-1%#", fileName, ext];
}
// get last match (theoretically the only one due to "$" in the regex)
NSTextCheckingResult *result = (NSTextCheckingResult *)[matches lastObject];
// extract suffix value
NSUInteger counterValue = [[fileName substringWithRange:[result rangeAtIndex:1]] integerValue];
// remove old suffix from the string
NSString *fileNameNoSuffix = [fileName stringByReplacingCharactersInRange:[result rangeAtIndex:0] withString:#""];
// return the path with the incremented counter suffix
return [NSString stringWithFormat:#"%#-%i%#", fileNameNoSuffix, counterValue + 1, ext];
}
... and the following are the tests I used:
- (void)testStringToAvoidNameCollisionForPath {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
// bad configs //
STAssertThrows([DMStringUtils stringToAvoidNameCollisionForPath:nil], nil);
STAssertThrows([DMStringUtils stringToAvoidNameCollisionForPath:#""], nil);
// files //
NSString *path = [bundle pathForResource:#"bar-0.abc" ofType:#"txt"];
NSString *savePath = [DMStringUtils stringToAvoidNameCollisionForPath:path];
STAssertEqualObjects([savePath lastPathComponent], #"bar-0.abc-1.txt", nil);
NSString *path1 = [bundle pathForResource:#"bar1" ofType:#"txt"];
NSString *savePath1 = [DMStringUtils stringToAvoidNameCollisionForPath:path1];
STAssertEqualObjects([savePath1 lastPathComponent], #"bar1-1.txt", nil);
NSString *path2 = [bundle pathForResource:#"bar51.foo.yeah1" ofType:#"txt"];
NSString *savePath2 = [DMStringUtils stringToAvoidNameCollisionForPath:path2];
STAssertEqualObjects([savePath2 lastPathComponent], #"bar51.foo.yeah1-1.txt", nil);
NSString *path3 = [path1 stringByDeletingLastPathComponent];
NSString *savePath3 = [DMStringUtils stringToAvoidNameCollisionForPath:[path3 stringByAppendingPathComponent:#"xxx.zip"]];
STAssertEqualObjects([savePath3 lastPathComponent], #"xxx.zip", nil);
NSString *path4 = [bundle pathForResource:#"foo.bar1-1-2-3-4" ofType:#"txt"];
NSString *savePath4 = [DMStringUtils stringToAvoidNameCollisionForPath:path4];
STAssertEqualObjects([savePath4 lastPathComponent], #"foo.bar1-1-2-3-5.txt", nil);
NSString *path5 = [bundle pathForResource:#"bar1-1" ofType:#"txt"];
NSString *savePath5 = [DMStringUtils stringToAvoidNameCollisionForPath:path5];
STAssertEqualObjects([savePath5 lastPathComponent], #"bar1-2.txt", nil);
// folders //
NSString *path6 = [DOCUMENTS_PATH stringByAppendingPathComponent:#"foo1"];
NSString *savePath6 = [DMStringUtils stringToAvoidNameCollisionForPath:path6];
STAssertEqualObjects([savePath6 lastPathComponent], #"foo1-1", nil);
NSString *path7 = [DOCUMENTS_PATH stringByAppendingPathComponent:#"bar1-1"];
NSString *savePath7 = [DMStringUtils stringToAvoidNameCollisionForPath:path7];
STAssertEqualObjects([savePath7 lastPathComponent], #"bar1-2", nil);
NSString *path8 = [DOCUMENTS_PATH stringByAppendingPathComponent:#"foo-5.bar123"];
NSString *savePath8 = [DMStringUtils stringToAvoidNameCollisionForPath:path8];
STAssertEqualObjects([savePath8 lastPathComponent], #"foo-5.bar123-1", nil);
}

Reading float from a custom file

I want to read some float value one by one from a custom file I defined "player.geo".
player.geo is a file I created using Xcode 4 ("Empty File" from the File > New menu)
I'm currently trying to do it like this:
- (id) initWithGeometryFile:(NSString *) nameOfFile
{
NSFileHandle *geoFile = NULL;
NSString *geoFilePath = [[NSBundle mainBundle] pathForResource:#"player" ofType:#"geo"];
geoFile = [NSFileHandle fileHandleForReadingAtPath:geoFilePath];
if(geoFile == NULL)
{
NSLog(#"Failed to open file.");
}
else
{
NSLog(#"Opening %# successful", nameOfFile);
NSMutableData *fileData = [[NSMutableData alloc] initWithData:[geoFile readDataOfLength:4]];
float firstValue;
[fileData getBytes:&firstValue length:sizeof(float)];
NSLog(#"First value in file %# is %f", nameOfFile, firstValue);
}
return self;
}
I'm not getting the expected value of -64.0, rather I'm getting 0.0.
Is this the right way to go about it?
Do I really have to read the file as a string and then parse float the string contents to get the float value?
NSData objects deal with raw bytes, not strings. If you are typing in a string into a txt file, this will not work. If you are using NSData objects, then you will need to first write the data using the data object methods such as writeToFile:atomically:.
Alternately, you can use the NSString functions stringWithContentsOfFile and componentsSeperatedByString to generate an NSArray containing each string on it's own line, like so:
NSString *tmp;
NSArray *lines;
lines = [[NSString stringWithContentsOfFile:#"testFileReadLines.txt"]
componentsSeparatedByString:#"\n"];
NSEnumerator *nse = [lines objectEnumerator];
while(tmp = [nse nextObject]) {
NSLog(#"%#", tmp);
}

Easy way to get size of folder (ObjC/Cocoa)?

Right now I'm using this code to get the size of a folder:
NSArray *contents;
NSEnumerator *enumerator;
NSString *path;
contents = [[NSFileManager defaultManager] subpathsAtPath:folderPath];
enumerator = [contents objectEnumerator];
while (path = [enumerator nextObject]) {
NSDictionary *fattrib = [[NSFileManager defaultManager] fileAttributesAtPath:[folderPath stringByAppendingPathComponent:path] traverseLink:YES];
fileSize +=[fattrib fileSize];
}
[contents release];
[path release];
The problem is that its highly innacurate. It either adds a few megabytes or deducts a few megabytes from the actual size. For example I got the file size of an .app bundle and this method reported 16.2MB, whereas the actual thing is 15.8.
What's the best way to get the size of a folder?
Thanks
I needed to do this today myself, and I've found that the code in this post on the Cocoa-dev list is super fast and matches what Finder says to the byte. (don't forget to OR in the kFSCatInfoRsrcSizes flag so you get resource fork sizes, too!)
If you need more explanation on how to use it, just leave a comment and I'll edit this post. =)
The documentation for fileSize states it does not include the size of a resource fork. You may need to use the Carbon File Manager API to reliably calculate directory sizes.
I just wanted to second Dave DeLong's suggestion about the post on Cocoa-dev, but add a cautionary note to be sure to read all the posts in the thread. There is one by Rosyna that's particularly worth noting. In my case I followed that advice (changing max items per fetch to 40) and saw a speed jump as well as the end to a nasty crashing bug.
hope this will help
- (unsigned long long) fastFolderSizeAtFSRef:(NSString *)theFilePath
{
unsigned long long totalSize = 0;
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isdirectory;
NSError *error;
if ([fileManager fileExistsAtPath:theFilePath])
{
NSMutableArray * directoryContents = [[fileManager contentsOfDirectoryAtPath:theFilePath error:&error] mutableCopy];
for (NSString *fileName in directoryContents)
{
if (([fileName rangeOfString:#".DS_Store"].location != NSNotFound) )
continue;
NSString *path = [theFilePath stringByAppendingPathComponent:fileName];
if([fileManager fileExistsAtPath:path isDirectory:&isdirectory] && isdirectory )
{
totalSize = totalSize + [self fastFolderSizeAtFSRef:path];
}
else
{
unsigned long long fileSize = [[fileManager attributesOfItemAtPath:path error:&error] fileSize];
totalSize = totalSize + fileSize;
}
}
}
return totalSize;
}
This is typically how it is done. 2 possibilities:
Check your byte -> megabyte conversion routines. Also, do you want megabytes or mebibytes? (It probably depends on what you're comparing it to.)
Try passing NO for the traverseLink parameter. There might very well be a symlink in the bundle pointing to something else that the routine you're comparing it to won't account for. You'll either count something in the bundle twice, or you'll include something outside the bundle entirely (most likely the former).
I know that this is an old topic. But for anyone out there looking for answers on how to do this,
[[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
if (isDir) {
NSPipe *pipe = [NSPipe pipe];
NSTask *t = [[[NSTask alloc] init] autorelease];
[t setLaunchPath:#"/usr/bin/du"];
[t setArguments:[NSArray arrayWithObjects:#"-k", #"-d", #"0", path, nil]];
[t setStandardOutput:pipe];
[t setStandardError:[NSPipe pipe]];
[t launch];
[t waitUntilExit];
NSString *sizeString = [[[NSString alloc] initWithData:[[pipe fileHandleForReading] availableData] encoding:NSASCIIStringEncoding] autorelease];
sizeString = [[sizeString componentsSeparatedByString:#" "] objectAtIndex:0];
bytes = [sizeString longLongValue]*1024;
}
else {
bytes = [[[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil] fileSize];
}
It will use terminal to determine a size for folders in bytes. And it will use Cocoa's built in NSFileManager to get the size of files. It's very fast, and gets the exact size that finder reports.
This code is as extension(category) to the NSFileManager class. It sums the sizes of all folder content.
Note that error treatment could be enhanced.
#interface NSFileManager(Util)
- (NSNumber *)sizeForFolderAtPath:(NSString *) source error:(NSError **)error;
#end
#implementation NSFileManager(Util)
- (NSNumber *)sizeForFolderAtPath:(NSString *) source error:(NSError **)error
{
NSArray * contents;
unsigned long long size = 0;
NSEnumerator * enumerator;
NSString * path;
BOOL isDirectory;
// Determine Paths to Add
if ([self fileExistsAtPath:source isDirectory:&isDirectory] && isDirectory)
{
contents = [self subpathsAtPath:source];
}
else
{
contents = [NSArray array];
}
// Add Size Of All Paths
enumerator = [contents objectEnumerator];
while (path = [enumerator nextObject])
{
NSDictionary * fattrs = [self attributesOfItemAtPath: [ source stringByAppendingPathComponent:path ] error:error];
size += [[fattrs objectForKey:NSFileSize] unsignedLongLongValue];
}
// Return Total Size in Bytes
return [ NSNumber numberWithUnsignedLongLong:size];
}
#end