7

I have written an android app that saves (potentially) large files to the SD Card. Occasionally I get an IOException during the write operation which causes the file to be left in a corrupt state. Based on the answer to this question:

Question: How to safely write to a file?

the strategy I should use is to create a temporary file and then copy this file once the write has completed. My questions are:

1) Is this the safest approach on Android? (e.g. Can you copy files on the android sd card and if so is the operation atomic?)

2) In an embedded system with limited resources (disk space) does anyone know of another strategy for safely writing to a disk? (instead of creating two large files)

Thanks

Community
  • 1
  • 1
brif
  • 443
  • 7
  • 11

4 Answers4

11

I know this is too late to answer, but will help anyone who come across this will find it helpful.

You need to use AtomicFile.

Following code snippet may help.

public void writeFile(File file, byte[] data) {
    AtomicFile aFile = new AtomicFile(file);
    FileOutputStream fos = null;

    try {
        fos = aFile.startWrite();
        fos.write(data);
        // Close stream and commit new data.
        aFile.finishWrite(fos);
        fos = null;
    } catch (IOException e) {
        Log.e(TAG, "Cannot write file " + file.getPath(), e);
    } finally {
        if (fos != null) {
            // Something went wrong.
            // Close stream and roll back data.
            aFile.failWrite(fos);
        }
    }
}
McMath
  • 6,862
  • 2
  • 28
  • 33
Pawan
  • 1,183
  • 16
  • 29
  • 2
    This answer is not late because it could not appear when the question was asked. AtomicFile was added in the API level 17 and the support library version 22.1.0. – beroal Dec 21 '18 at 19:32
  • Shouldn't aFile.failWrite(fos) be inside catch block instead of finally??? – Chandler May 13 '22 at 04:16
10

The typical way to safely create a file on most reasonable platforms (Linux/Android is one of them) is to create a temporary file and then rename the file, as mentioned in the question & answers that you linked to. Note the emphasis on rename; renaming a file is usually an atomic operation within the same filesystem, copying one is not. In addition, this method only requires enough space for a single copy of the data.

Therefore you create a file in the target directory using a temporary name and then use File.renameTo() to give it a proper name. By using a standard naming convention for the temporary files you can always find and delete them, even if your application terminates unexpectedly due to e.g. a device power-off.

If you are really paranoid, you may want to insert a few calls to FileDescriptor.sync() or equivalent...

EDIT:

BTW, you do not mention what kind of IOException your are getting and whether you have tracked down its cause. If it's due to insufficient space, fine, but if we are talking about a faulty SD card, then there is no such thing as "safe" in this case.

EDIT 2:

In order to check the available free space, you can create a File object for the destination directory (i.e. the directory where your file will end up) and call File.getFreeSpace(). Keep in mind that this check does not provide any guarantees - you may still end up without enough space if e.g. another process writes data to the medium.

thkala
  • 84,049
  • 23
  • 157
  • 201
  • Thanks for the clear answer. Rename makes so much sense, I had missed that in the other question. – brif Oct 30 '11 at 18:08
  • Renaming definitely sounds like the answer. However I was wondering you can check if you have enough empty space on the SD for the file you are preparing to write? – Jeff Oct 31 '11 at 01:44
  • @Jeff: you can call [`File.getFreeSpace()`](http://developer.android.com/reference/java/io/File.html#getFreeSpace%28%29) to avoid the more obvious cases of insufficient space. – thkala Oct 31 '11 at 11:15
  • 1
    It should be noted that this is a safer way of *overwriting* a file only when knowing the new file is complete. This is no safer than writing the file directly if it's a new file. – NKijak Nov 02 '11 at 02:29
  • 1
    @NKijak: it *is* a safer way for creating new files, in the sense that the file get its final filename if and only if it is created and filled in successfully. Otherwise all that remains is a temporary name e.g. `MyApplication.tmp.XNdd3f` that can be easily found and deleted. Even if the application crashes before it handles any errors, there is no way for an incomplete/corrupt file to end up with a proper filename that might be inadvertently used. – thkala Nov 02 '11 at 07:42
0

As mentioned in Pawan's answer, AtomicFile is recommended for writing file safely.

But you still need to be careful when using it, sometimes it's not that 'atomic'.

You can find AtomicFile in android.util, android.support.v4.util and androidx.core.util. But their implementation are DIFFERENT in different versions of API level / support library / androidx library, so please pay attention to your dependency version before using it.

In general, you should use the latest version of androidx's AtomicFile. It's in androidx.core:core, and dependenced by androidx.appcompat:appcompat.


Let's see what actually happens when we try to write a file with AtomicFile in lower versions.

API level: [17, 29] / support library: [22.1.0, 28.0.0] / androidx core: [1.0.0, 1.3.2]

public FileOutputStream startWrite() throws IOException {
    // Step 1: rename base file to backup file, or delete it.
    if (mBaseName.exists()) {
        if (!mBackupName.exists()) {
            if (!mBaseName.renameTo(mBackupName)) {
                Log.w("AtomicFile", "Couldn't rename file " + mBaseName
                    + " to backup file " + mBackupName);
                }
        } else {
            mBaseName.delete();
        }
    }

    // At this moment,the base file does not exist anyway.

    // Step 2: open output stream to base file.
    FileOutputStream str;
    try {
        str = new FileOutputStream(mBaseName);
    } catch (FileNotFoundException e) {
        // ...
    }
    return str;
}

As you can see, AtomicFile prefers to back up the base file before writing it. This can indeed provide an effective means of recovery when writing to the base file fails. But you can only say that the write operation is safely, not atomically.

Imagine what happens if the system loses power between steps 1 and 2, the base file will be lost, and only the backup file will remain.

Also, because the FileOutputStream points to the base file, writing is not atomically either.


But google has improved the implementation of AtomicFile in high versions.

API level: 30+ / androidx core: 1.5.0+

public FileOutputStream startWrite() throws IOException {
    // Step 1: recover backup file, which was used in old version.
    if (mLegacyBackupName.exists()) {
        rename(mLegacyBackupName, mBaseName);
    }

    // Step 2: open output stream to new file, not the base file.
    try {
        return new FileOutputStream(mNewName);
    } catch (FileNotFoundException e) {
        // ...
    }
}

The improved AtomicFile takes another approach: when you write content to the output stream returned by startWrite(), you actually write to a new file (a temporary file with suffix of .new), and the new file will be rename to base file after finishWrite() called.

Now, the content of the base file will only have two cases:

  1. The writing fails, and the content of base file remains unchanged.
  2. The writing is successful.

It's real atomic now.

Orienser
  • 11
  • 1
0

I don't know if I'd copy it, but renaming the temporary file is typical. If you get an exception during writing, just delete the temporary file.

Dave Newton
  • 158,873
  • 26
  • 254
  • 302