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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With