Saturday, April 30, 2011

How can I safely write to a given file path in Cocoa, adding a numeric suffix if necessary?

We want to write to "foo.txt" in a given directory. If "foo.txt" already exists, we want to write to "foo-1.txt", and so on.

There are a few code snippets around that try and answer this question, but none are quite satisfactory. E.g. this solution at CocoaDev uses NSFileManager to test if a path exists to create a safe path. However, this leads to obvious race conditions between obtaining a path and writing to it. It would be safer to attempt atomic writes, and loop the numeric suffix on failure.

Go at it!

From stackoverflow
  • Use the open system call with the O_EXCL and O_CREAT options. If the file doesn't already exist, open will create it, open it, and return the file descriptor to you; if it does exist, open will fail and set errno to EEXIST.

    From there, it should be obvious how to construct the loop that tries incrementing filenames until it returns a file descriptor or constructs a filename too long. On the latter point, make sure you check errno when open fails—EEXIST and ENAMETOOLONG are just two of the errors you could encounter.

  • int fd;
    uint32_t counter;
    char filename[1024]; // obviously unsafe
    
    sprintf(filename, "foo.txt");
    if( (fd = open(filename, O_CREAT | O_EXCL | O_EXLOCK, 0644)) == -1 && errno == EEXIST ) 
    {
        for( counter = 1; counter < UINT32_MAX; counter++ ) {
          sprintf(filename, "foo-%u.txt", counter);
          if( (fd = open(filename, O_CREAT | O_EXCL | O_EXLOCK, 0644)) == -1 && errno == EEXIST )
            continue;
          else
            break;
        }
    }
    
    if( fd == -1 && counter == UINT32_MAX ) {
     fprintf(stderr, "too many foo-files\n");
    } else if( fd == -1 ) {
     fprintf(stderr, "could not open file: %s\n", strerror(errno));
    }
    
    // otherwise fd is an open file with an atomically unique name and an
    // exclusive lock.
    
  • How about:

    1. Write the file to a temporary directory where you know there's no risk of collision
    2. Use NSFileManager to move the file to the preferred destination
    3. If step 3 fails due to a file already existing, add/increment a numeric suffix and repeat step 2

    You'd be basically re-creating Cocoa's atomic file write handling, but adding in the feature of ensuring a unique filename. A big advantage of this approach is that if the power goes out or your app crashes mid-write, the half-finished file will be tucked away in a tmp folder and deleted by the system; not left for the user to try and work with.

0 comments:

Post a Comment