| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.os; |
| |
| import android.annotation.UnsupportedAppUsage; |
| import android.os.Build; |
| import android.os.FileUtils; |
| import android.util.Log; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| |
| /** |
| * Helper class for performing atomic operations on a file, by creating a |
| * backup file until a write has successfully completed. |
| * <p> |
| * Atomic file guarantees file integrity by ensuring that a file has |
| * been completely written and sync'd to disk before removing its backup. |
| * As long as the backup file exists, the original file is considered |
| * to be invalid (left over from a previous attempt to write the file). |
| * </p><p> |
| * Atomic file does not confer any file locking semantics. |
| * Do not use this class when the file may be accessed or modified concurrently |
| * by multiple threads or processes. The caller is responsible for ensuring |
| * appropriate mutual exclusion invariants whenever it accesses the file. |
| * </p> |
| */ |
| public final class AtomicFile { |
| private final File mBaseName; |
| private final File mBackupName; |
| |
| @UnsupportedAppUsage |
| public AtomicFile(File baseName) { |
| mBaseName = baseName; |
| mBackupName = new File(baseName.getPath() + ".bak"); |
| } |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
| public File getBaseFile() { |
| return mBaseName; |
| } |
| |
| @UnsupportedAppUsage |
| public FileOutputStream startWrite() throws IOException { |
| // Rename the current file so it may be used as a backup during the next read |
| 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(); |
| } |
| } |
| FileOutputStream str = null; |
| try { |
| str = new FileOutputStream(mBaseName); |
| } catch (FileNotFoundException e) { |
| File parent = mBaseName.getParentFile(); |
| if (!parent.mkdir()) { |
| throw new IOException("Couldn't create directory " + mBaseName); |
| } |
| FileUtils.setPermissions( |
| parent.getPath(), |
| FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, |
| -1, -1); |
| try { |
| str = new FileOutputStream(mBaseName); |
| } catch (FileNotFoundException e2) { |
| throw new IOException("Couldn't create " + mBaseName); |
| } |
| } |
| return str; |
| } |
| |
| @UnsupportedAppUsage |
| public void finishWrite(FileOutputStream str) { |
| if (str != null) { |
| FileUtils.sync(str); |
| try { |
| str.close(); |
| mBackupName.delete(); |
| } catch (IOException e) { |
| Log.w("AtomicFile", "finishWrite: Got exception:", e); |
| } |
| } |
| } |
| |
| @UnsupportedAppUsage |
| public void failWrite(FileOutputStream str) { |
| if (str != null) { |
| FileUtils.sync(str); |
| try { |
| str.close(); |
| mBaseName.delete(); |
| mBackupName.renameTo(mBaseName); |
| } catch (IOException e) { |
| Log.w("AtomicFile", "failWrite: Got exception:", e); |
| } |
| } |
| } |
| |
| @UnsupportedAppUsage |
| public FileOutputStream openAppend() throws IOException { |
| try { |
| return new FileOutputStream(mBaseName, true); |
| } catch (FileNotFoundException e) { |
| throw new IOException("Couldn't append " + mBaseName); |
| } |
| } |
| |
| @UnsupportedAppUsage |
| public void truncate() throws IOException { |
| try { |
| FileOutputStream fos = new FileOutputStream(mBaseName); |
| FileUtils.sync(fos); |
| fos.close(); |
| } catch (FileNotFoundException e) { |
| throw new IOException("Couldn't append " + mBaseName); |
| } catch (IOException e) { |
| } |
| } |
| |
| public boolean exists() { |
| return mBaseName.exists() || mBackupName.exists(); |
| } |
| |
| public void delete() { |
| mBaseName.delete(); |
| mBackupName.delete(); |
| } |
| |
| @UnsupportedAppUsage |
| public FileInputStream openRead() throws FileNotFoundException { |
| if (mBackupName.exists()) { |
| mBaseName.delete(); |
| mBackupName.renameTo(mBaseName); |
| } |
| return new FileInputStream(mBaseName); |
| } |
| |
| @UnsupportedAppUsage |
| public byte[] readFully() throws IOException { |
| FileInputStream stream = openRead(); |
| try { |
| int pos = 0; |
| int avail = stream.available(); |
| byte[] data = new byte[avail]; |
| while (true) { |
| int amt = stream.read(data, pos, data.length-pos); |
| //Log.i("foo", "Read " + amt + " bytes at " + pos |
| // + " of avail " + data.length); |
| if (amt <= 0) { |
| //Log.i("foo", "**** FINISHED READING: pos=" + pos |
| // + " len=" + data.length); |
| return data; |
| } |
| pos += amt; |
| avail = stream.available(); |
| if (avail > data.length-pos) { |
| byte[] newData = new byte[pos+avail]; |
| System.arraycopy(data, 0, newData, 0, pos); |
| data = newData; |
| } |
| } |
| } finally { |
| stream.close(); |
| } |
| } |
| } |