简体   繁体   中英

How to safely save a file to disk in android?

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? (eg 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

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 eg 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 (ie 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 eg another process writes data to the medium.

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.

private void writeFile(File file, byte[] data) {
    file.mkdirs();
    try {
        AtomicFile aFile = new AtomicFile(file);
        FileOutputStream fos = aFile.startWrite();
        fos.write(data);
        aFile.finishWrite(fos);
    } catch (IOException ioe) {
        Log.e(TAG, "Cannot write file " + file.getPath());
    }
}

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM