How to overwrite a file with NSFileManager when copying?
Asked Answered
T

10

63

I'm using this method to copy a file:

[fileManager copyItemAtPath:sourcePath toPath:targetPath error:&error];

I want to overwrite a file when it exists already. The default behavior of this method is to throw an exception/error "File Exists." when the file exists. There's no option to specify that it should overwrite.

So what would be the safest way to do this?

Would I first check if the file exists, then delete it, and then attempt to copy? This has the danger that the app or device goes OFF right in the nanosecond after the file has been deleted but the new file hasn't been copied to that place. Then there's nothing.

Maybe I would have to change the name of the new file first, then delete the old, and then re-change the name of the new? Same problem. What if in this nanosecond the app or device goes OFF and renaming doesn't happen?

Thickset answered 26/5, 2011 at 10:57 Comment(0)
P
24

You'd want to do an atomic save in this case, which would be best achieved by using NSData or NSString's writeToFile:atomically: methods (and their variants):

NSData *myData = ...; //fetched from somewhere
[myData writeToFile:targetPath atomically:YES];

Or for an NSString:

NSString *myString = ...;
NSError *err = nil;
[myString writeToFile:targetPath atomically:YES encoding:NSUTF8StringEncoding error:&err];
if(err != nil) {
  //we have an error.
}
Pat answered 26/5, 2011 at 11:2 Comment(4)
What if you don't want to load the entire file into RAM (due to it being large)? Loading the file seems like it could be somewhat inefficient.Woolly
@NickForge You could use NSFileManager's -moveItemAtPath:toPath:error: method.Pat
@JacobRelkin This method does not override existing files so it cannot be used.Jollanta
Agreed, but is it atomic?Alloplasm
L
55

If you can't/don't want to keep the file contents in memory but want an atomic rewrite as noted in the other suggestions, you can first copy the original file to a temp directory to a unique path (Apple's documentation suggests using a temporary directory), then use NSFileManager's

-replaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error:

According to the reference documentation, this method 'replaces the contents of the item at the specified URL in a manner that insures no data loss occurs.' (from reference documentation). The copying of the original to the temporary directory is needed because this method moves the original file. Here's the NSFileManager reference documentation about -replaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error:

Letters answered 9/3, 2012 at 13:46 Comment(4)
Great answer. Upvoted. What if the file is simultaneously being processed in some way, like being sent to another app via a UIDocumentInteractionController, or being printed via AirPrint, or being uploaded to a server? Does this ensure that the second app (or the printer or the server) will get either the new file or the old one, rather than a corrupted file consisting of some bits from the new file and some bits from the old one? Or is that assumption not safe to make?Alloplasm
@KartickVaddadi this method gives guarantees that the file that appears at the target location is placed there atomically. I am presuming that concurrent modifications to the source data would sitll lead to corruption. If that is a concern, you should probably wrap operations on the source file through NSFileCoordinator / NSFilePresenter APIs – at least on iOS you should not be getting modifications from the system that aren't NSFileCoordinator proof.Letters
I'm talking about changes to the destination, not source. I understand atomicity wrt to (say) the system losing power, but what if you have a file handle / stream open to the target file? How does atomicity affect that? Say you were uploading "a.pdf" to a server, and halfway through the upload, the file gets replaced by "b.pdf". Will the server have a.pdf, b.pdf, or a corrupt file? Likewise for printing or sharing to another app. If the answer is that it will be corrupt, should one wrap all operations on the target in file coordination?Alloplasm
I'm pretty sure this works at least on HFS+ by replacing the inode on the filesystem, which would probably mean that attempting to write to file handles that were open until that inode replacement happened would begin failing on the file writing system calls (< guess). Uploads would be a perfect case of wanting atomic file operations because you don't want to upload in place into the final URL a file that may never finish uploading successfully. Also, as there's no filesystem locking facility, file coordination is indeed a pretty useful thing often when you have potential for multiple writers.Letters
C
26

If you're not sure if the file exists, this works on swift 3+

try? FileManager.default.removeItem(at: item_destination)
try FileManager.default.copyItem(at: item, to: item_destination)

The first line fails and is ignored if the file doesn't already exist. If there's a exception during the second line, it throws as it should.

Carrero answered 19/9, 2018 at 16:50 Comment(2)
thank you. Replacement removed source item for me. With your suggestion it works greatCorium
If the program is terminated between two lines of code, you will not find the file in place, which may result in a large area of crash! ⚠️Don't do this in a production environment!Rossuck
P
24

You'd want to do an atomic save in this case, which would be best achieved by using NSData or NSString's writeToFile:atomically: methods (and their variants):

NSData *myData = ...; //fetched from somewhere
[myData writeToFile:targetPath atomically:YES];

Or for an NSString:

NSString *myString = ...;
NSError *err = nil;
[myString writeToFile:targetPath atomically:YES encoding:NSUTF8StringEncoding error:&err];
if(err != nil) {
  //we have an error.
}
Pat answered 26/5, 2011 at 11:2 Comment(4)
What if you don't want to load the entire file into RAM (due to it being large)? Loading the file seems like it could be somewhat inefficient.Woolly
@NickForge You could use NSFileManager's -moveItemAtPath:toPath:error: method.Pat
@JacobRelkin This method does not override existing files so it cannot be used.Jollanta
Agreed, but is it atomic?Alloplasm
M
11

Swift4:

_ = try FileManager.default.replaceItemAt(previousItemUrl, withItemAt: currentItemUrl)
Macias answered 3/4, 2018 at 17:15 Comment(3)
While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value.Eldaelden
To add context, since this is the best answer on the page, it will replace the file if it exists, and if not will save the file there regardless. It returns the url of the new item or nil.Phagy
Meh, it might be the best answer if you're doing swift but the OP isn't coding in swift.Doorway
T
5

Detect file exists error, delete the destination file and copy again.

Sample code in Swift 2.0:

class MainWindowController: NSFileManagerDelegate {

    let fileManager = NSFileManager()

    override func windowDidLoad() {
        super.windowDidLoad()
        fileManager.delegate = self
        do {
            try fileManager.copyItemAtPath(srcPath, toPath: dstPath)
        } catch {
            print("File already exists at \'\(srcPath)\':\n\((error as NSError).description)")
        }
    }

    func fileManager(fileManager: NSFileManager, shouldProceedAfterError error: NSError, copyingItemAtPath srcPath: String, toPath dstPath: String) -> Bool {
        if error.code == NSFileWriteFileExistsError {
            do {
                try fileManager.removeItemAtPath(dstPath)
                print("Existing file deleted.")
            } catch {
                print("Failed to delete existing file:\n\((error as NSError).description)")
            }
            do {
                try fileManager.copyItemAtPath(srcPath, toPath: dstPath)
                print("File saved.")
            } catch {
                print("File not saved:\n\((error as NSError).description)")
            }
            return true
        } else {
            return false
        }
    }
}
Tamaratamarack answered 4/10, 2015 at 7:4 Comment(1)
In Swift 2.0, the better option would be to use the do/try/catch syntax instead of "try!" and using the delegate.Monacid
E
4

For overwriting files, I prefer

NSData *imgDta = UIImageJPEGRepresentation(tImg, 1.0);

[imgDta writeToFile:targetPath options:NSDataWritingFileProtectionNone error:&err];

Removing & copying files in loop sometimes doesn't work as intended

Electrotype answered 6/8, 2014 at 14:11 Comment(0)
B
1

I think what you're looking for is the NSFileManagerDelegate protocol method:

- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error copyingItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

From this method, you can decide what to do with the existing file (rename/delete) and then proceed with the copy.

Benuecongo answered 3/12, 2014 at 19:31 Comment(0)
L
0

I think the possibility of the nanosecond you mensioned is feeble. so stick to the first method of removing the existing file and copying the new file.

Lifton answered 26/5, 2011 at 11:4 Comment(1)
I believe that nanosecond is dependent on the size of data being written to disk ;)Olio
C
0

This is for improvement of 'Swift 3 and above' of question 'Move file and override [duplicate]' which is marked duplicate of this question.

To move file from sourcepath(string) to DestinationPath(string). Delete the existing file if same name file already exists at DestinationPath.

// Set the correct path in string in 'let' variables.
let destinationStringPath = ""
let sourceStringPath = ""

let fileManager:FileManager = FileManager.default
do
{
    try fileManager.removeItem(atPath: sourceStringPath)
}
catch
{
}

do
{
    try fileManager.moveItem(atPath: sourceStringPath, toPath: destinationStringPath)
}
catch
{
}
Confined answered 16/4, 2018 at 9:47 Comment(0)
R
0

The following code ensures that the copy will succeed without file corruption or loss, even if the application is terminated at any time.

if try FileManager.default.fileExists(atPath: target.path) == true {
    try fileManager.replaceItemAt(target, withItemAt: source)
}
Ravelin answered 4/5, 2022 at 16:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.