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