• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tradefed.util;
18 
19 import com.android.tradefed.build.BuildRetrievalError;
20 import com.android.tradefed.build.IFileDownloader;
21 import com.android.tradefed.log.LogUtil.CLog;
22 
23 import com.google.api.client.googleapis.json.GoogleJsonResponseException;
24 import com.google.api.services.storage.Storage;
25 import com.google.api.services.storage.model.Objects;
26 import com.google.api.services.storage.model.StorageObject;
27 import com.google.common.annotations.VisibleForTesting;
28 
29 import java.io.ByteArrayInputStream;
30 import java.io.ByteArrayOutputStream;
31 import java.io.File;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.nio.file.Paths;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.Collections;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Set;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45 
46 /** File downloader to download file from google cloud storage (GCS). */
47 public class GCSFileDownloader extends GCSCommon implements IFileDownloader {
48     public static final String GCS_PREFIX = "gs://";
49     public static final String GCS_APPROX_PREFIX = "gs:/";
50 
51     private static final Pattern GCS_PATH_PATTERN = Pattern.compile("gs://([^/]*)/(.*)");
52     private static final String PATH_SEP = "/";
53     private static final Collection<String> SCOPES =
54             Collections.singleton("https://www.googleapis.com/auth/devstorage.read_only");
55     private static final long LIST_BATCH_SIZE = 100;
56 
GCSFileDownloader(File jsonKeyFile)57     public GCSFileDownloader(File jsonKeyFile) {
58         super(jsonKeyFile);
59     }
60 
GCSFileDownloader()61     public GCSFileDownloader() {}
62 
63     /**
64      * Download a file from a GCS bucket file.
65      *
66      * @param bucketName GCS bucket name
67      * @param filename the filename
68      * @return {@link InputStream} with the file content.
69      */
downloadFile(String bucketName, String filename)70     public InputStream downloadFile(String bucketName, String filename) throws IOException {
71         InputStream remoteInput = null;
72         ByteArrayOutputStream tmpStream = null;
73         try {
74             remoteInput =
75                     getStorage().objects().get(bucketName, filename).executeMediaAsInputStream();
76             // The input stream from api call can not be reset. Change it to ByteArrayInputStream.
77             tmpStream = new ByteArrayOutputStream();
78             StreamUtil.copyStreams(remoteInput, tmpStream);
79             return new ByteArrayInputStream(tmpStream.toByteArray());
80         } finally {
81             StreamUtil.close(remoteInput);
82             StreamUtil.close(tmpStream);
83         }
84     }
85 
getStorage()86     private Storage getStorage() throws IOException {
87         return getStorage(SCOPES);
88     }
89 
90     @VisibleForTesting
getRemoteFileMetaData(String bucketName, String remoteFilename)91     StorageObject getRemoteFileMetaData(String bucketName, String remoteFilename)
92             throws IOException {
93         try {
94             return getStorage().objects().get(bucketName, remoteFilename).execute();
95         } catch (GoogleJsonResponseException e) {
96             if (e.getStatusCode() == 404) {
97                 return null;
98             }
99             throw e;
100         }
101     }
102 
103     /**
104      * Download file from GCS.
105      *
106      * <p>Right now only support GCS path.
107      *
108      * @param remoteFilePath gs://bucket/file/path format GCS path.
109      * @return local file
110      * @throws BuildRetrievalError
111      */
112     @Override
downloadFile(String remoteFilePath)113     public File downloadFile(String remoteFilePath) throws BuildRetrievalError {
114         File destFile = createTempFile(remoteFilePath, null);
115         try {
116             downloadFile(remoteFilePath, destFile);
117             return destFile;
118         } catch (BuildRetrievalError e) {
119             FileUtil.recursiveDelete(destFile);
120             throw e;
121         }
122     }
123 
124     @Override
downloadFile(String remotePath, File destFile)125     public void downloadFile(String remotePath, File destFile) throws BuildRetrievalError {
126         String[] pathParts = parseGcsPath(remotePath);
127         downloadFile(pathParts[0], pathParts[1], destFile);
128     }
129 
isFileFresh(File localFile, StorageObject remoteFile)130     private boolean isFileFresh(File localFile, StorageObject remoteFile) throws IOException {
131         if (localFile == null && remoteFile == null) {
132             return true;
133         }
134         if (localFile == null || remoteFile == null) {
135             return false;
136         }
137         if (!localFile.exists()) {
138             return false;
139         }
140         return remoteFile.getMd5Hash().equals(FileUtil.calculateBase64Md5(localFile));
141     }
142 
143     @Override
isFresh(File localFile, String remotePath)144     public boolean isFresh(File localFile, String remotePath) throws BuildRetrievalError {
145         String[] pathParts = parseGcsPath(remotePath);
146         String bucketName = pathParts[0];
147         String remoteFilename = pathParts[1];
148         try {
149             StorageObject remoteFileMeta = getRemoteFileMetaData(bucketName, remoteFilename);
150             if (localFile == null || !localFile.exists()) {
151                 if (!isRemoteFolder(bucketName, remoteFilename) && remoteFileMeta == null) {
152                     // The local doesn't exist and the remote filename is not a folder or a file.
153                     return true;
154                 }
155                 return false;
156             }
157             if (!localFile.isDirectory()) {
158                 return isFileFresh(localFile, remoteFileMeta);
159             }
160             remoteFilename = sanitizeDirectoryName(remoteFilename);
161             return recursiveCheckFolderFreshness(bucketName, remoteFilename, localFile);
162         } catch (IOException e) {
163             throw new BuildRetrievalError(e.getMessage(), e);
164         }
165     }
166 
167     /**
168      * Check if remote folder is the same as local folder, recursively. The remoteFolderName must
169      * end with "/".
170      *
171      * @param bucketName is the gcs bucket name.
172      * @param remoteFolderName is the relative path to the bucket.
173      * @param localFolder is the local folder
174      * @return true if local file is the same as remote file, otherwise false.
175      * @throws IOException
176      */
recursiveCheckFolderFreshness( String bucketName, String remoteFolderName, File localFolder)177     private boolean recursiveCheckFolderFreshness(
178             String bucketName, String remoteFolderName, File localFolder) throws IOException {
179         Set<String> subFilenames = new HashSet<>(Arrays.asList(localFolder.list()));
180         List<String> subRemoteFolders = new ArrayList<>();
181         List<StorageObject> subRemoteFiles = new ArrayList<>();
182         listRemoteFilesUnderFolder(bucketName, remoteFolderName, subRemoteFiles, subRemoteFolders);
183         for (StorageObject subRemoteFile : subRemoteFiles) {
184             String subFilename = Paths.get(subRemoteFile.getName()).getFileName().toString();
185             if (!isFileFresh(new File(localFolder, subFilename), subRemoteFile)) {
186                 return false;
187             }
188             subFilenames.remove(subFilename);
189         }
190         for (String subRemoteFolder : subRemoteFolders) {
191             String subFolderName = Paths.get(subRemoteFolder).getFileName().toString();
192             if (!recursiveCheckFolderFreshness(
193                     bucketName, subRemoteFolder, new File(localFolder, subFolderName))) {
194                 return false;
195             }
196             subFilenames.remove(subFolderName);
197         }
198         return subFilenames.isEmpty();
199     }
200 
listRemoteFilesUnderFolder( String bucketName, String folder, List<StorageObject> subFiles, List<String> subFolders)201     void listRemoteFilesUnderFolder(
202             String bucketName, String folder, List<StorageObject> subFiles, List<String> subFolders)
203             throws IOException {
204         String pageToken = null;
205         while (true) {
206             com.google.api.services.storage.Storage.Objects.List listOperation =
207                     getStorage()
208                             .objects()
209                             .list(bucketName)
210                             .setPrefix(folder)
211                             .setDelimiter(PATH_SEP)
212                             .setMaxResults(LIST_BATCH_SIZE);
213             if (pageToken != null) {
214                 listOperation.setPageToken(pageToken);
215             }
216             Objects objects = listOperation.execute();
217             if (objects.getItems() != null && !objects.getItems().isEmpty()) {
218                 subFiles.addAll(objects.getItems());
219             }
220             if (objects.getPrefixes() != null && !objects.getPrefixes().isEmpty()) {
221                 subFolders.addAll(objects.getPrefixes());
222             }
223             pageToken = objects.getNextPageToken();
224             if (pageToken == null) {
225                 return;
226             }
227         }
228     }
229 
parseGcsPath(String remotePath)230     String[] parseGcsPath(String remotePath) throws BuildRetrievalError {
231         if (remotePath.startsWith(GCS_APPROX_PREFIX) && !remotePath.startsWith(GCS_PREFIX)) {
232             // File object remove double // so we have to rebuild it in some cases
233             remotePath = remotePath.replaceAll(GCS_APPROX_PREFIX, GCS_PREFIX);
234         }
235         Matcher m = GCS_PATH_PATTERN.matcher(remotePath);
236         if (!m.find()) {
237             throw new BuildRetrievalError(
238                     String.format("Only GCS path is supported, %s is not supported", remotePath));
239         }
240         return new String[] {m.group(1), m.group(2)};
241     }
242 
sanitizeDirectoryName(String name)243     String sanitizeDirectoryName(String name) {
244         /** Folder name should end with "/" */
245         if (!name.endsWith(PATH_SEP)) {
246             name += PATH_SEP;
247         }
248         return name;
249     }
250 
251     /** check given filename is a folder or not. */
252     @VisibleForTesting
isRemoteFolder(String bucketName, String filename)253     boolean isRemoteFolder(String bucketName, String filename) throws IOException {
254         filename = sanitizeDirectoryName(filename);
255         Objects objects =
256                 getStorage()
257                         .objects()
258                         .list(bucketName)
259                         .setPrefix(filename)
260                         .setDelimiter(PATH_SEP)
261                         .setMaxResults(1l)
262                         .execute();
263         if (objects.getItems() != null && !objects.getItems().isEmpty()) {
264             return true;
265         }
266         if (objects.getPrefixes() != null && !objects.getPrefixes().isEmpty()) {
267             return true;
268         }
269         return false;
270     }
271 
272     @VisibleForTesting
downloadFile(String bucketName, String remoteFilename, File localFile)273     void downloadFile(String bucketName, String remoteFilename, File localFile)
274             throws BuildRetrievalError {
275         try {
276             if (!isRemoteFolder(bucketName, remoteFilename)) {
277                 fetchRemoteFile(bucketName, remoteFilename, localFile);
278                 return;
279             }
280             remoteFilename = sanitizeDirectoryName(remoteFilename);
281             recursiveDownloadFolder(bucketName, remoteFilename, localFile);
282         } catch (IOException e) {
283             CLog.e("Failed to download gs://%s/%s, clean up.", bucketName, remoteFilename);
284             throw new BuildRetrievalError(e.getMessage(), e);
285         }
286     }
287 
fetchRemoteFile(String bucketName, String remoteFilename, File localFile)288     private void fetchRemoteFile(String bucketName, String remoteFilename, File localFile)
289             throws IOException {
290         getStorage()
291                 .objects()
292                 .get(bucketName, remoteFilename)
293                 .executeMediaAndDownloadTo(new FileOutputStream(localFile));
294     }
295 
296     /**
297      * Recursively download remote folder to local folder.
298      *
299      * @param bucketName the gcs bucket name
300      * @param remoteFolderName remote folder name, must end with "/"
301      * @param localFolder local folder
302      * @throws IOException
303      */
recursiveDownloadFolder( String bucketName, String remoteFolderName, File localFolder)304     private void recursiveDownloadFolder(
305             String bucketName, String remoteFolderName, File localFolder) throws IOException {
306         CLog.d("Downloading folder gs://%s/%s.", bucketName, remoteFolderName);
307         if (!localFolder.exists()) {
308             FileUtil.mkdirsRWX(localFolder);
309         }
310         Set<String> subFilenames = new HashSet<>(Arrays.asList(localFolder.list()));
311         List<String> subRemoteFolders = new ArrayList<>();
312         List<StorageObject> subRemoteFiles = new ArrayList<>();
313         listRemoteFilesUnderFolder(bucketName, remoteFolderName, subRemoteFiles, subRemoteFolders);
314         for (StorageObject subRemoteFile : subRemoteFiles) {
315             String subFilename = Paths.get(subRemoteFile.getName()).getFileName().toString();
316             fetchRemoteFile(
317                     bucketName, subRemoteFile.getName(), new File(localFolder, subFilename));
318             subFilenames.remove(subFilename);
319         }
320         for (String subRemoteFolder : subRemoteFolders) {
321             String subFolderName = Paths.get(subRemoteFolder).getFileName().toString();
322             recursiveDownloadFolder(
323                     bucketName, subRemoteFolder, new File(localFolder, subFolderName));
324             subFilenames.remove(subFolderName);
325         }
326         for (String subFilename : subFilenames) {
327             FileUtil.recursiveDelete(new File(localFolder, subFilename));
328         }
329     }
330 
331     /**
332      * Creates a unique file on temporary disk to house downloaded file with given path.
333      *
334      * <p>Constructs the file name based on base file name from path
335      *
336      * @param remoteFilePath the remote path to construct the name from
337      */
338     @VisibleForTesting
createTempFile(String remoteFilePath, File rootDir)339     File createTempFile(String remoteFilePath, File rootDir) throws BuildRetrievalError {
340         try {
341             // create a unique file.
342             File tmpFile = FileUtil.createTempFileForRemote(remoteFilePath, rootDir);
343             // now delete it so name is available
344             tmpFile.delete();
345             return tmpFile;
346         } catch (IOException e) {
347             String msg = String.format("Failed to create tmp file for %s", remoteFilePath);
348             throw new BuildRetrievalError(msg, e);
349         }
350     }
351 }
352