1 // Copyright 2021 The Android Open Source Project 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.google.android.downloader; 16 17 import static com.google.common.base.Preconditions.checkState; 18 19 import com.google.common.io.Files; 20 import java.io.File; 21 import java.io.IOException; 22 import java.io.RandomAccessFile; 23 import java.nio.channels.FileChannel; 24 import java.nio.channels.WritableByteChannel; 25 26 /** 27 * Basic implementation of {@link DownloadDestination} which streams the download to a {@link File}. 28 * 29 * <p>This implementation also keeps track of metadata in a separate file, encoded as a protocol 30 * buffer message. Note that this implementation isn't especially robust - concurrent reads and 31 * writes could result in errors or in data corruption, and invalid/corrupt metadata will result in 32 * persistent errors. 33 */ 34 public class ProtoFileDownloadDestination implements DownloadDestination { 35 private final File targetFile; 36 private final File metadataFile; 37 ProtoFileDownloadDestination(File targetFile, File metadataFile)38 public ProtoFileDownloadDestination(File targetFile, File metadataFile) { 39 this.targetFile = targetFile; 40 this.metadataFile = metadataFile; 41 } 42 43 @Override numExistingBytes()44 public long numExistingBytes() throws IOException { 45 return targetFile.length(); 46 } 47 48 @Override readMetadata()49 public DownloadMetadata readMetadata() throws IOException { 50 return metadataFile.exists() 51 ? readMetadataFromBytes(Files.toByteArray(metadataFile)) 52 : DownloadMetadata.create(); 53 } 54 55 @Override openByteChannel(long offsetBytes, DownloadMetadata metadata)56 public WritableByteChannel openByteChannel(long offsetBytes, DownloadMetadata metadata) 57 throws IOException { 58 checkState( 59 offsetBytes <= targetFile.length(), 60 "Opening byte channel with offset past known end of file"); 61 Files.write(writeMetadataToBytes(metadata), metadataFile); 62 FileChannel fileChannel = new RandomAccessFile(targetFile, "rw").getChannel(); 63 // Seek to the requested offset, so we can append data rather than overwrite data. 64 fileChannel.position(offsetBytes); 65 return fileChannel; 66 } 67 68 @Override clear()69 public void clear() throws IOException { 70 if (targetFile.exists() && !targetFile.delete()) { 71 throw new IOException("Failed to delete()"); 72 } 73 } 74 75 @Override toString()76 public String toString() { 77 return targetFile.toString(); 78 } 79 readMetadataFromBytes(byte[] bytes)80 private static DownloadMetadata readMetadataFromBytes(byte[] bytes) throws IOException { 81 DownloadMetadataProto proto = DownloadMetadataProto.parseFrom(bytes); 82 return DownloadMetadata.create(proto.getContentTag(), proto.getLastModifiedTimeSeconds()); 83 } 84 writeMetadataToBytes(DownloadMetadata metadata)85 private static byte[] writeMetadataToBytes(DownloadMetadata metadata) { 86 DownloadMetadataProto.Builder builder = DownloadMetadataProto.newBuilder(); 87 88 if (!metadata.getContentTag().isEmpty()) { 89 builder.setContentTag(metadata.getContentTag()); 90 } 91 if (metadata.getLastModifiedTimeSeconds() > 0) { 92 builder.setLastModifiedTimeSeconds(metadata.getLastModifiedTimeSeconds()); 93 } 94 95 return builder.build().toByteArray(); 96 } 97 } 98