mirror of
https://github.com/samsonjs/media.git
synced 2026-04-15 12:55:46 +00:00
Encrypt SimpleCache index file.
Clean up AtomicFile and make it return a custom FileOutputStream for writing which handles IOException automatically during write operations. It also syncs the file descriptor and deletes the backup file on close() call. This fixes the order of flush and close operations when the fileoutputstream is wrapped by another OutputStream. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138779187
This commit is contained in:
parent
16ddc84d93
commit
92a98d1ce2
10 changed files with 443 additions and 212 deletions
|
|
@ -8,9 +8,11 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
/**
|
||||
* Tests {@link CachedContentIndex}.
|
||||
|
|
@ -91,21 +93,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
}
|
||||
|
||||
public void testStoreAndLoad() throws Exception {
|
||||
index.addNew(new CachedContent(5, "key1", 10));
|
||||
index.add("key2");
|
||||
|
||||
index.store();
|
||||
|
||||
CachedContentIndex index2 = new CachedContentIndex(cacheDir);
|
||||
index2.load();
|
||||
|
||||
Set<String> keys = index.getKeys();
|
||||
Set<String> keys2 = index2.getKeys();
|
||||
assertEquals(keys, keys2);
|
||||
for (String key : keys) {
|
||||
assertEquals(index.getContentLength(key), index2.getContentLength(key));
|
||||
assertEquals(index.get(key).getSpans(), index2.get(key).getSpans());
|
||||
}
|
||||
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
|
||||
}
|
||||
|
||||
public void testLoadV1() throws Exception {
|
||||
|
|
@ -168,4 +156,66 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
assertEquals(1, CachedContentIndex.getNewId(idToKey));
|
||||
}
|
||||
|
||||
public void testEncryption() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
byte[] key2 = "bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
|
||||
new CachedContentIndex(cacheDir, key));
|
||||
|
||||
// Rename the index file from the test above
|
||||
File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME);
|
||||
File file2 = new File(cacheDir, "file2compare");
|
||||
assertTrue(file1.renameTo(file2));
|
||||
|
||||
// Write a new index file
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
|
||||
new CachedContentIndex(cacheDir, key));
|
||||
|
||||
assertEquals(file2.length(), file1.length());
|
||||
// Assert file content is different
|
||||
FileInputStream fis1 = new FileInputStream(file1);
|
||||
FileInputStream fis2 = new FileInputStream(file2);
|
||||
for (int b; (b = fis1.read()) == fis2.read();) {
|
||||
assertTrue(b != -1);
|
||||
}
|
||||
|
||||
boolean threw = false;
|
||||
try {
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
|
||||
new CachedContentIndex(cacheDir, key2));
|
||||
} catch (AssertionFailedError e) {
|
||||
threw = true;
|
||||
}
|
||||
assertTrue("Encrypted index file can not be read with different encryption key", threw);
|
||||
|
||||
try {
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
|
||||
new CachedContentIndex(cacheDir));
|
||||
} catch (AssertionFailedError e) {
|
||||
threw = true;
|
||||
}
|
||||
assertTrue("Encrypted index file can not be read without encryption key", threw);
|
||||
|
||||
// Non encrypted index file can be read even when encryption key provided.
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
|
||||
new CachedContentIndex(cacheDir, key));
|
||||
}
|
||||
|
||||
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
|
||||
throws IOException {
|
||||
index.addNew(new CachedContent(5, "key1", 10));
|
||||
index.add("key2");
|
||||
index.store();
|
||||
|
||||
index2.load();
|
||||
Set<String> keys = index.getKeys();
|
||||
Set<String> keys2 = index2.getKeys();
|
||||
assertEquals(keys, keys2);
|
||||
for (String key : keys) {
|
||||
assertEquals(index.getContentLength(key), index2.getContentLength(key));
|
||||
assertEquals(index.get(key).getSpans(), index2.get(key).getSpans());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (C) 2016 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.google.android.exoplayer2.util;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Tests {@link AtomicFile}.
|
||||
*/
|
||||
public class AtomicFileTest extends InstrumentationTestCase {
|
||||
|
||||
private File tempFolder;
|
||||
private File file;
|
||||
private AtomicFile atomicFile;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
file = new File(tempFolder, "atomicFile");
|
||||
atomicFile = new AtomicFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(tempFolder);
|
||||
}
|
||||
|
||||
public void testDelete() throws Exception {
|
||||
assertTrue(file.createNewFile());
|
||||
atomicFile.delete();
|
||||
assertFalse(file.exists());
|
||||
}
|
||||
|
||||
public void testWriteEndRead() throws Exception {
|
||||
OutputStream output = atomicFile.startWrite();
|
||||
output.write(5);
|
||||
atomicFile.endWrite(output);
|
||||
output.close();
|
||||
|
||||
assertRead();
|
||||
|
||||
output = atomicFile.startWrite();
|
||||
output.write(5);
|
||||
output.write(6);
|
||||
output.close();
|
||||
|
||||
assertRead();
|
||||
|
||||
output = atomicFile.startWrite();
|
||||
output.write(6);
|
||||
|
||||
assertRead();
|
||||
|
||||
output = atomicFile.startWrite();
|
||||
|
||||
assertRead();
|
||||
}
|
||||
|
||||
private void assertRead() throws IOException {
|
||||
InputStream input = atomicFile.openRead();
|
||||
assertEquals(5, input.read());
|
||||
assertEquals(-1, input.read());
|
||||
input.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
|
@ -60,6 +61,21 @@ public interface Cache {
|
|||
void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when an error is encountered when writing data.
|
||||
*/
|
||||
class CacheException extends IOException {
|
||||
|
||||
public CacheException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CacheException(IOException cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to listen for changes to a given key.
|
||||
|
|
@ -125,7 +141,7 @@ public interface Cache {
|
|||
* @return The {@link CacheSpan}.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
CacheSpan startReadWrite(String key, long position) throws InterruptedException;
|
||||
CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException;
|
||||
|
||||
/**
|
||||
* Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then
|
||||
|
|
@ -135,7 +151,7 @@ public interface Cache {
|
|||
* @param position The position of the data being requested.
|
||||
* @return The {@link CacheSpan}. Or null if the cache entry is locked.
|
||||
*/
|
||||
CacheSpan startReadWriteNonBlocking(String key, long position);
|
||||
CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException;
|
||||
|
||||
/**
|
||||
* Obtains a cache file into which data can be written. Must only be called when holding a
|
||||
|
|
@ -147,7 +163,7 @@ public interface Cache {
|
|||
* is enough space in the cache.
|
||||
* @return The file into which data should be written.
|
||||
*/
|
||||
File startFile(String key, long position, long maxLength);
|
||||
File startFile(String key, long position, long maxLength) throws CacheException;
|
||||
|
||||
/**
|
||||
* Commits a file into the cache. Must only be called when holding a corresponding hole
|
||||
|
|
@ -155,7 +171,7 @@ public interface Cache {
|
|||
*
|
||||
* @param file A newly written cache file.
|
||||
*/
|
||||
void commitFile(File file);
|
||||
void commitFile(File file) throws CacheException;
|
||||
|
||||
/**
|
||||
* Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which
|
||||
|
|
@ -170,7 +186,7 @@ public interface Cache {
|
|||
*
|
||||
* @param span The {@link CacheSpan} to remove.
|
||||
*/
|
||||
void removeSpan(CacheSpan span);
|
||||
void removeSpan(CacheSpan span) throws CacheException;
|
||||
|
||||
/**
|
||||
* Queries if a range is entirely available in the cache.
|
||||
|
|
@ -188,7 +204,7 @@ public interface Cache {
|
|||
* @param key The cache key for the data.
|
||||
* @param length The length of the data.
|
||||
*/
|
||||
void setContentLength(String key, long length);
|
||||
void setContentLength(String key, long length) throws CacheException;
|
||||
|
||||
/**
|
||||
* Returns the content length for the given key if one set, or {@link
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ package com.google.android.exoplayer2.upstream.cache;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.upstream.DataSink;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ public final class CacheDataSink implements DataSink {
|
|||
/**
|
||||
* Thrown when IOException is encountered when writing data into sink.
|
||||
*/
|
||||
public static class CacheDataSinkException extends IOException {
|
||||
public static class CacheDataSinkException extends CacheException {
|
||||
|
||||
public CacheDataSinkException(IOException cause) {
|
||||
super(cause);
|
||||
|
|
@ -50,7 +50,6 @@ public final class CacheDataSink implements DataSink {
|
|||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param cache The cache into which data should be written.
|
||||
* @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
|
||||
|
|
@ -71,7 +70,7 @@ public final class CacheDataSink implements DataSink {
|
|||
dataSpecBytesWritten = 0;
|
||||
try {
|
||||
openNextOutputStream();
|
||||
} catch (FileNotFoundException e) {
|
||||
} catch (IOException e) {
|
||||
throw new CacheDataSinkException(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +111,7 @@ public final class CacheDataSink implements DataSink {
|
|||
}
|
||||
}
|
||||
|
||||
private void openNextOutputStream() throws FileNotFoundException {
|
||||
private void openNextOutputStream() throws IOException {
|
||||
file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten,
|
||||
Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize));
|
||||
outputStream = new FileOutputStream(file);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import com.google.android.exoplayer2.upstream.DataSourceException;
|
|||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.upstream.TeeDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.lang.annotation.Retention;
|
||||
|
|
@ -328,7 +328,7 @@ public final class CacheDataSource implements DataSource {
|
|||
return successful;
|
||||
}
|
||||
|
||||
private void setContentLength(long length) {
|
||||
private void setContentLength(long length) throws IOException {
|
||||
cache.setContentLength(key, length);
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +349,7 @@ public final class CacheDataSource implements DataSource {
|
|||
}
|
||||
|
||||
private void handleBeforeThrow(IOException exception) {
|
||||
if (currentDataSource == cacheReadDataSource || exception instanceof CacheDataSinkException) {
|
||||
if (currentDataSource == cacheReadDataSource || exception instanceof CacheException) {
|
||||
seenCacheError = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
|
|
@ -150,14 +151,18 @@ import java.util.TreeSet;
|
|||
*
|
||||
* @param cacheSpan Span to be copied and updated.
|
||||
* @return a span with the updated last access time.
|
||||
* @throws CacheException If renaming of the underlying span file failed.
|
||||
*/
|
||||
public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) {
|
||||
public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) throws CacheException {
|
||||
// Remove the old span from the in-memory representation.
|
||||
Assertions.checkState(cachedSpans.remove(cacheSpan));
|
||||
// Obtain a new span with updated last access timestamp.
|
||||
SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id);
|
||||
// Rename the cache file
|
||||
cacheSpan.file.renameTo(newCacheSpan.file);
|
||||
if (!cacheSpan.file.renameTo(newCacheSpan.file)) {
|
||||
throw new CacheException("Renaming of " + cacheSpan.file + " to " + newCacheSpan.file
|
||||
+ " failed.");
|
||||
}
|
||||
// Add the updated span back into the in-memory representation.
|
||||
cachedSpans.add(newCacheSpan);
|
||||
return newCacheSpan;
|
||||
|
|
|
|||
|
|
@ -17,18 +17,30 @@ package com.google.android.exoplayer2.upstream.cache;
|
|||
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.AtomicFile;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* This class maintains the index of cached content.
|
||||
|
|
@ -36,15 +48,36 @@ import java.util.Set;
|
|||
/*package*/ final class CachedContentIndex {
|
||||
|
||||
public static final String FILE_NAME = "cached_content_index.exi";
|
||||
|
||||
private static final int VERSION = 1;
|
||||
|
||||
private static final int FLAG_ENCRYPTED_INDEX = 1;
|
||||
|
||||
private final HashMap<String, CachedContent> keyToContent;
|
||||
private final SparseArray<String> idToKey;
|
||||
private final AtomicFile atomicFile;
|
||||
private final Cipher cipher;
|
||||
private final SecretKeySpec secretKeySpec;
|
||||
private boolean changed;
|
||||
|
||||
/** Creates a CachedContentIndex which works on the index file in the given cacheDir. */
|
||||
public CachedContentIndex(File cacheDir) {
|
||||
this(cacheDir, null);
|
||||
}
|
||||
|
||||
/** Creates a CachedContentIndex which works on the index file in the given cacheDir. */
|
||||
public CachedContentIndex(File cacheDir, byte[] secretKey) {
|
||||
if (secretKey != null) {
|
||||
try {
|
||||
cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
|
||||
secretKeySpec = new SecretKeySpec(secretKey, "AES");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalStateException(e); // Should never happen.
|
||||
}
|
||||
} else {
|
||||
cipher = null;
|
||||
secretKeySpec = null;
|
||||
}
|
||||
keyToContent = new HashMap<>();
|
||||
idToKey = new SparseArray<>();
|
||||
atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
|
||||
|
|
@ -53,18 +86,15 @@ import java.util.Set;
|
|||
/** Loads the index file. */
|
||||
public void load() {
|
||||
Assertions.checkState(!changed);
|
||||
File cacheIndex = atomicFile.getBaseFile();
|
||||
if (cacheIndex.exists()) {
|
||||
if (!readFile()) {
|
||||
cacheIndex.delete();
|
||||
keyToContent.clear();
|
||||
idToKey.clear();
|
||||
}
|
||||
if (!readFile()) {
|
||||
atomicFile.delete();
|
||||
keyToContent.clear();
|
||||
idToKey.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/** Stores the index data to index file if there is a change. */
|
||||
public void store() {
|
||||
public void store() throws CacheException {
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -177,13 +207,30 @@ import java.util.Set;
|
|||
private boolean readFile() {
|
||||
DataInputStream input = null;
|
||||
try {
|
||||
input = new DataInputStream(atomicFile.openRead());
|
||||
InputStream inputStream = atomicFile.openRead();
|
||||
input = new DataInputStream(inputStream);
|
||||
int version = input.readInt();
|
||||
if (version != VERSION) {
|
||||
// Currently there is no other version
|
||||
return false;
|
||||
}
|
||||
input.readInt(); // ignore flags placeholder
|
||||
|
||||
int flags = input.readInt();
|
||||
if ((flags & FLAG_ENCRYPTED_INDEX) != 0) {
|
||||
if (cipher == null) {
|
||||
return false;
|
||||
}
|
||||
byte[] initializationVector = new byte[16];
|
||||
input.read(initializationVector);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
input = new DataInputStream(new CipherInputStream(inputStream, cipher));
|
||||
}
|
||||
|
||||
int count = input.readInt();
|
||||
int hashCode = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
|
|
@ -204,14 +251,30 @@ import java.util.Set;
|
|||
return true;
|
||||
}
|
||||
|
||||
private void writeFile() {
|
||||
FileOutputStream outputStream = null;
|
||||
private void writeFile() throws CacheException {
|
||||
DataOutputStream output = null;
|
||||
try {
|
||||
outputStream = atomicFile.startWrite();
|
||||
DataOutputStream output = new DataOutputStream(outputStream);
|
||||
|
||||
OutputStream outputStream = atomicFile.startWrite();
|
||||
output = new DataOutputStream(outputStream);
|
||||
output.writeInt(VERSION);
|
||||
output.writeInt(0); // flags placeholder
|
||||
|
||||
int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0;
|
||||
output.writeInt(flags);
|
||||
|
||||
if (cipher != null) {
|
||||
byte[] initializationVector = new byte[16];
|
||||
new Random().nextBytes(initializationVector);
|
||||
output.write(initializationVector);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalStateException(e); // Should never happen.
|
||||
}
|
||||
output.flush();
|
||||
output = new DataOutputStream(new CipherOutputStream(outputStream, cipher));
|
||||
}
|
||||
|
||||
output.writeInt(keyToContent.size());
|
||||
int hashCode = 0;
|
||||
for (CachedContent cachedContent : keyToContent.values()) {
|
||||
|
|
@ -219,12 +282,11 @@ import java.util.Set;
|
|||
hashCode += cachedContent.headerHashCode();
|
||||
}
|
||||
output.writeInt(hashCode);
|
||||
|
||||
output.flush();
|
||||
atomicFile.finishWrite(outputStream);
|
||||
atomicFile.endWrite(output);
|
||||
} catch (IOException e) {
|
||||
atomicFile.failWrite(outputStream);
|
||||
throw new RuntimeException("Writing the new cache index file failed.", e);
|
||||
throw new CacheException(e);
|
||||
} finally {
|
||||
Util.closeQuietly(output);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import java.util.Comparator;
|
||||
import java.util.TreeSet;
|
||||
|
||||
|
|
@ -74,7 +75,11 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar
|
|||
|
||||
private void evictCache(Cache cache, long requiredSpace) {
|
||||
while (currentSize + requiredSpace > maxBytes) {
|
||||
cache.removeSpan(leastRecentlyUsed.first());
|
||||
try {
|
||||
cache.removeSpan(leastRecentlyUsed.first());
|
||||
} catch (CacheException e) {
|
||||
// do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,18 +38,33 @@ public final class SimpleCache implements Cache {
|
|||
private final CachedContentIndex index;
|
||||
private final HashMap<String, ArrayList<Listener>> listeners;
|
||||
private long totalSpace = 0;
|
||||
private CacheException initializationException;
|
||||
|
||||
/**
|
||||
* Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
|
||||
* the directory cannot be used to store other files.
|
||||
*
|
||||
* @param cacheDir A dedicated cache directory.
|
||||
* @param evictor The evictor to be used.
|
||||
*/
|
||||
public SimpleCache(File cacheDir, CacheEvictor evictor) {
|
||||
this(cacheDir, evictor, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
|
||||
* the directory cannot be used to store other files.
|
||||
*
|
||||
* @param cacheDir A dedicated cache directory.
|
||||
* @param evictor The evictor to be used.
|
||||
* @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
|
||||
* The key must be 16 bytes long.
|
||||
*/
|
||||
public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) {
|
||||
this.cacheDir = cacheDir;
|
||||
this.evictor = evictor;
|
||||
this.lockedSpans = new HashMap<>();
|
||||
this.index = new CachedContentIndex(cacheDir);
|
||||
this.index = new CachedContentIndex(cacheDir, secretKey);
|
||||
this.listeners = new HashMap<>();
|
||||
// Start cache initialization.
|
||||
final ConditionVariable conditionVariable = new ConditionVariable();
|
||||
|
|
@ -58,7 +73,11 @@ public final class SimpleCache implements Cache {
|
|||
public void run() {
|
||||
synchronized (SimpleCache.this) {
|
||||
conditionVariable.open();
|
||||
initialize();
|
||||
try {
|
||||
initialize();
|
||||
} catch (CacheException e) {
|
||||
initializationException = e;
|
||||
}
|
||||
SimpleCache.this.evictor.onCacheInitialized();
|
||||
}
|
||||
}
|
||||
|
|
@ -106,7 +125,7 @@ public final class SimpleCache implements Cache {
|
|||
|
||||
@Override
|
||||
public synchronized SimpleCacheSpan startReadWrite(String key, long position)
|
||||
throws InterruptedException {
|
||||
throws InterruptedException, CacheException {
|
||||
while (true) {
|
||||
SimpleCacheSpan span = startReadWriteNonBlocking(key, position);
|
||||
if (span != null) {
|
||||
|
|
@ -122,7 +141,12 @@ public final class SimpleCache implements Cache {
|
|||
}
|
||||
|
||||
@Override
|
||||
public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) {
|
||||
public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position)
|
||||
throws CacheException {
|
||||
if (initializationException != null) {
|
||||
throw initializationException;
|
||||
}
|
||||
|
||||
SimpleCacheSpan cacheSpan = getSpan(key, position);
|
||||
|
||||
// Read case.
|
||||
|
|
@ -144,7 +168,8 @@ public final class SimpleCache implements Cache {
|
|||
}
|
||||
|
||||
@Override
|
||||
public synchronized File startFile(String key, long position, long maxLength) {
|
||||
public synchronized File startFile(String key, long position, long maxLength)
|
||||
throws CacheException {
|
||||
Assertions.checkState(lockedSpans.containsKey(key));
|
||||
if (!cacheDir.exists()) {
|
||||
// For some reason the cache directory doesn't exist. Make a best effort to create it.
|
||||
|
|
@ -157,7 +182,7 @@ public final class SimpleCache implements Cache {
|
|||
}
|
||||
|
||||
@Override
|
||||
public synchronized void commitFile(File file) {
|
||||
public synchronized void commitFile(File file) throws CacheException {
|
||||
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index);
|
||||
Assertions.checkState(span != null);
|
||||
Assertions.checkState(lockedSpans.containsKey(span.key));
|
||||
|
|
@ -199,7 +224,7 @@ public final class SimpleCache implements Cache {
|
|||
* @param position The position of the span being requested.
|
||||
* @return The corresponding cache {@link SimpleCacheSpan}.
|
||||
*/
|
||||
private SimpleCacheSpan getSpan(String key, long position) {
|
||||
private SimpleCacheSpan getSpan(String key, long position) throws CacheException {
|
||||
CachedContent cachedContent = index.get(key);
|
||||
if (cachedContent == null) {
|
||||
return SimpleCacheSpan.createOpenHole(key, position);
|
||||
|
|
@ -219,7 +244,7 @@ public final class SimpleCache implements Cache {
|
|||
/**
|
||||
* Ensures that the cache's in-memory representation has been initialized.
|
||||
*/
|
||||
private void initialize() {
|
||||
private void initialize() throws CacheException {
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
return;
|
||||
|
|
@ -259,7 +284,7 @@ public final class SimpleCache implements Cache {
|
|||
notifySpanAdded(span);
|
||||
}
|
||||
|
||||
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) {
|
||||
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException {
|
||||
CachedContent cachedContent = index.get(span.key);
|
||||
Assertions.checkState(cachedContent.removeSpan(span));
|
||||
totalSpace -= span.length;
|
||||
|
|
@ -271,7 +296,7 @@ public final class SimpleCache implements Cache {
|
|||
}
|
||||
|
||||
@Override
|
||||
public synchronized void removeSpan(CacheSpan span) {
|
||||
public synchronized void removeSpan(CacheSpan span) throws CacheException {
|
||||
removeSpan(span, true);
|
||||
}
|
||||
|
||||
|
|
@ -279,7 +304,7 @@ public final class SimpleCache implements Cache {
|
|||
* Scans all of the cached spans in the in-memory representation, removing any for which files
|
||||
* no longer exist.
|
||||
*/
|
||||
private void removeStaleSpansAndCachedContents() {
|
||||
private void removeStaleSpansAndCachedContents() throws CacheException {
|
||||
LinkedList<CacheSpan> spansToBeRemoved = new LinkedList<>();
|
||||
for (CachedContent cachedContent : index.getAll()) {
|
||||
for (CacheSpan span : cachedContent.getSpans()) {
|
||||
|
|
@ -336,7 +361,7 @@ public final class SimpleCache implements Cache {
|
|||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setContentLength(String key, long length) {
|
||||
public synchronized void setContentLength(String key, long length) throws CacheException {
|
||||
index.setContentLength(key, length);
|
||||
index.store();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
* Copyright (C) 2016 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.
|
||||
|
|
@ -22,192 +22,176 @@ import java.io.FileInputStream;
|
|||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Exoplayer internal version of the framework's {@link android.util.AtomicFile},
|
||||
* a 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>
|
||||
* A 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>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.
|
||||
*/
|
||||
public class AtomicFile {
|
||||
private final File mBaseName;
|
||||
private final File mBackupName;
|
||||
public final class AtomicFile {
|
||||
|
||||
private static final String TAG = "AtomicFile";
|
||||
|
||||
private final File baseName;
|
||||
private final File backupName;
|
||||
|
||||
/**
|
||||
* Create a new AtomicFile for a file located at the given File path.
|
||||
* The secondary backup file will be the same file path with ".bak" appended.
|
||||
* Create a new AtomicFile for a file located at the given File path. The secondary backup file
|
||||
* will be the same file path with ".bak" appended.
|
||||
*/
|
||||
public AtomicFile(File baseName) {
|
||||
mBaseName = baseName;
|
||||
mBackupName = new File(baseName.getPath() + ".bak");
|
||||
this.baseName = baseName;
|
||||
backupName = new File(baseName.getPath() + ".bak");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path to the base file. You should not generally use this,
|
||||
* as the data at that path may not be valid.
|
||||
*/
|
||||
public File getBaseFile() {
|
||||
return mBaseName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the atomic file. This deletes both the base and backup files.
|
||||
*/
|
||||
/** Delete the atomic file. This deletes both the base and backup files. */
|
||||
public void delete() {
|
||||
mBaseName.delete();
|
||||
mBackupName.delete();
|
||||
baseName.delete();
|
||||
backupName.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new write operation on the file. This returns a FileOutputStream
|
||||
* to which you can write the new file data. The existing file is replaced
|
||||
* with the new data. You <em>must not</em> directly close the given
|
||||
* FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)}
|
||||
* or {@link #failWrite(FileOutputStream)}.
|
||||
* Start a new write operation on the file. This returns an {@link OutputStream} to which you can
|
||||
* write the new file data. If the whole data is written successfully you <em>must</em> call
|
||||
* {@link #endWrite(OutputStream)}. On failure you should call {@link OutputStream#close()}
|
||||
* only to free up resources used by it.
|
||||
*
|
||||
* <p>Note that if another thread is currently performing
|
||||
* a write, this will simply replace whatever that thread is writing
|
||||
* with the new file being written by this thread, and when the other
|
||||
* thread finishes the write the new write operation will no longer be
|
||||
* safe (or will be lost). You must do your own threading protection for
|
||||
* access to AtomicFile.
|
||||
* <p>Example usage:
|
||||
*
|
||||
* <pre>
|
||||
* DataOutputStream dataOutput = null;
|
||||
* try {
|
||||
* OutputStream outputStream = atomicFile.startWrite();
|
||||
* dataOutput = new DataOutputStream(outputStream); // Wrapper stream
|
||||
* dataOutput.write(data1);
|
||||
* dataOutput.write(data2);
|
||||
* atomicFile.endWrite(dataOutput); // Pass wrapper stream
|
||||
* } finally{
|
||||
* if (dataOutput != null) {
|
||||
* dataOutput.close();
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Note that if another thread is currently performing a write, this will simply replace
|
||||
* whatever that thread is writing with the new file being written by this thread, and when the
|
||||
* other thread finishes the write the new write operation will no longer be safe (or will be
|
||||
* lost). You must do your own threading protection for access to AtomicFile.
|
||||
*/
|
||||
public FileOutputStream startWrite() throws IOException {
|
||||
public OutputStream 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);
|
||||
if (baseName.exists()) {
|
||||
if (!backupName.exists()) {
|
||||
if (!baseName.renameTo(backupName)) {
|
||||
Log.w(TAG, "Couldn't rename file " + baseName + " to backup file " + backupName);
|
||||
}
|
||||
} else {
|
||||
mBaseName.delete();
|
||||
baseName.delete();
|
||||
}
|
||||
}
|
||||
FileOutputStream str = null;
|
||||
OutputStream str = null;
|
||||
try {
|
||||
str = new FileOutputStream(mBaseName);
|
||||
str = new AtomicFileOutputStream(baseName);
|
||||
} catch (FileNotFoundException e) {
|
||||
File parent = mBaseName.getParentFile();
|
||||
File parent = baseName.getParentFile();
|
||||
if (!parent.mkdirs()) {
|
||||
throw new IOException("Couldn't create directory " + mBaseName);
|
||||
throw new IOException("Couldn't create directory " + baseName);
|
||||
}
|
||||
try {
|
||||
str = new FileOutputStream(mBaseName);
|
||||
str = new AtomicFileOutputStream(baseName);
|
||||
} catch (FileNotFoundException e2) {
|
||||
throw new IOException("Couldn't create " + mBaseName);
|
||||
throw new IOException("Couldn't create " + baseName);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when you have successfully finished writing to the stream
|
||||
* returned by {@link #startWrite()}. This will close, sync, and
|
||||
* commit the new data. The next attempt to read the atomic file
|
||||
* will return the new file stream.
|
||||
*/
|
||||
public void finishWrite(FileOutputStream str) {
|
||||
if (str != null) {
|
||||
sync(str);
|
||||
try {
|
||||
str.close();
|
||||
mBackupName.delete();
|
||||
} catch (IOException e) {
|
||||
Log.w("AtomicFile", "finishWrite: Got exception:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when you have failed for some reason at writing to the stream
|
||||
* returned by {@link #startWrite()}. This will close the current
|
||||
* write stream, and roll back to the previous state of the file.
|
||||
*/
|
||||
public void failWrite(FileOutputStream str) {
|
||||
if (str != null) {
|
||||
sync(str);
|
||||
try {
|
||||
str.close();
|
||||
mBaseName.delete();
|
||||
mBackupName.renameTo(mBaseName);
|
||||
} catch (IOException e) {
|
||||
Log.w("AtomicFile", "failWrite: Got exception:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the atomic file for reading. If there previously was an
|
||||
* incomplete write, this will roll back to the last good data before
|
||||
* opening for read. You should call close() on the FileInputStream when
|
||||
* you are done reading from it.
|
||||
* Call when you have successfully finished writing to the stream returned by {@link
|
||||
* #startWrite()}. This will close, sync, and commit the new data. The next attempt to read the
|
||||
* atomic file will return the new file stream.
|
||||
*
|
||||
* <p>Note that if another thread is currently performing
|
||||
* a write, this will incorrectly consider it to be in the state of a bad
|
||||
* write and roll back, causing the new data currently being written to
|
||||
* be dropped. You must do your own threading protection for access to
|
||||
* AtomicFile.
|
||||
* @param str Outer-most wrapper OutputStream used to write to the stream returned by {@link
|
||||
* #startWrite()}.
|
||||
* @see #startWrite()
|
||||
*/
|
||||
public FileInputStream openRead() throws FileNotFoundException {
|
||||
if (mBackupName.exists()) {
|
||||
mBaseName.delete();
|
||||
mBackupName.renameTo(mBaseName);
|
||||
}
|
||||
return new FileInputStream(mBaseName);
|
||||
public void endWrite(OutputStream str) throws IOException {
|
||||
str.close();
|
||||
// If close() throws exception, the next line is skipped.
|
||||
backupName.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience for {@link #openRead()} that also reads all of the
|
||||
* file contents into a byte array which is returned.
|
||||
* Open the atomic file for reading. If there previously was an incomplete write, this will roll
|
||||
* back to the last good data before opening for read.
|
||||
*
|
||||
* <p>Note that if another thread is currently performing a write, this will incorrectly consider
|
||||
* it to be in the state of a bad write and roll back, causing the new data currently being
|
||||
* written to be dropped. You must do your own threading protection for access to AtomicFile.
|
||||
*/
|
||||
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();
|
||||
public InputStream openRead() throws FileNotFoundException {
|
||||
restoreBackup();
|
||||
return new FileInputStream(baseName);
|
||||
}
|
||||
|
||||
private void restoreBackup() {
|
||||
if (backupName.exists()) {
|
||||
baseName.delete();
|
||||
backupName.renameTo(baseName);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sync(FileOutputStream stream) {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.getFD().sync();
|
||||
}
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
// do nothing
|
||||
private static final class AtomicFileOutputStream extends OutputStream {
|
||||
|
||||
private final FileOutputStream fileOutputStream;
|
||||
private boolean closed = false;
|
||||
|
||||
public AtomicFileOutputStream(File file) throws FileNotFoundException {
|
||||
fileOutputStream = new FileOutputStream(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
flush();
|
||||
try {
|
||||
fileOutputStream.getFD().sync();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to sync file descriptor:", e);
|
||||
}
|
||||
fileOutputStream.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
fileOutputStream.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
fileOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
fileOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
fileOutputStream.write(b, off, len);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue