Home | History | Annotate | Download | only in util
      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.ddmlib.Log;
     20 import com.android.tradefed.log.LogUtil.CLog;
     21 
     22 import java.io.File;
     23 import java.io.IOException;
     24 import java.nio.file.Path;
     25 import java.nio.file.Paths;
     26 import java.util.ArrayList;
     27 import java.util.List;
     28 import java.util.concurrent.TimeUnit;
     29 
     30 /**
     31  * File manager to download and upload files from Google Cloud Storage (GCS).
     32  *
     33  * This class should NOT be used from the scope of a test (i.e., IRemoteTest).
     34  */
     35 public class GCSBucketUtil {
     36 
     37     // https://cloud.google.com/storage/docs/gsutil
     38 
     39     private static final String CMD_COPY = "cp";
     40     private static final String CMD_MAKE_BUCKET = "mb";
     41     private static final String CMD_REMOVE = "rm";
     42     private static final String CMD_REMOVE_BUCKET = "rb";
     43     private static final String CMD_VERSION = "-v";
     44     private static final String ENV_BOTO_PATH = "BOTO_PATH";
     45     private static final String ENV_BOTO_CONFIG = "BOTO_CONFIG";
     46     private static final String FILENAME_STDOUT = "-";
     47     private static final String FLAG_FORCE = "-f";
     48     private static final String FLAG_NO_CLOBBER = "-n";
     49     private static final String FLAG_PARALLEL = "-m";
     50     private static final String FLAG_PROJECT_ID = "-p";
     51     private static final String FLAG_RECURSIVE = "-r";
     52     private static final String GCS_SCHEME = "gs";
     53     private static final String GSUTIL = "gsutil";
     54 
     55     /**
     56      * Whether gsutil is verified to be installed
     57      */
     58     private static boolean mCheckedGsutil = false;
     59 
     60     /**
     61      * Number of attempts for gsutil operations.
     62      *
     63      * @see RunUtil#runTimedCmdRetry
     64      */
     65     private int mAttempts = 1;
     66 
     67     /**
     68      * Path to the .boto files to use, set via environment variable $BOTO_PATH.
     69      *
     70      * @see <a href="https://cloud.google.com/storage/docs/gsutil/commands/config">
     71      *      gsutil documentation</a>
     72      */
     73     private String mBotoPath = null;
     74 
     75     /**
     76      * Path to the .boto file to use, set via environment variable $BOTO_CONFIG.
     77      *
     78      * @see <a href="https://cloud.google.com/storage/docs/gsutil/commands/config">
     79      *      gsutil documentation</a>
     80      */
     81     private String mBotoConfig = null;
     82 
     83     /**
     84      * Name of the GCS bucket.
     85      */
     86     private String mBucketName = null;
     87 
     88     /**
     89      * Whether to use the "-n" flag to avoid clobbering files.
     90      */
     91     private boolean mNoClobber = false;
     92 
     93     /**
     94      * Whether to use the "-m" flag to parallelize large operations.
     95      */
     96     private boolean mParallel = false;
     97 
     98     /**
     99      * Whether to use the "-r" flag to perform a recursive copy.
    100      */
    101     private boolean mRecursive = true;
    102 
    103     /**
    104      * Retry interval for gsutil operations.
    105      *
    106      * @see RunUtil#runTimedCmdRetry
    107      */
    108     private long mRetryInterval = 0;
    109 
    110     /**
    111      * Timeout for gsutil operations.
    112      *
    113      * @see RunUtil#runTimedCmdRetry
    114      */
    115     private long mTimeoutMs = 0;
    116 
    117     public GCSBucketUtil(String bucketName) {
    118         setBucketName(bucketName);
    119     }
    120 
    121     /**
    122      * Verify that gsutil is installed.
    123      */
    124     void checkGSUtil() throws IOException {
    125         if (mCheckedGsutil) {
    126             return;
    127         }
    128 
    129         // N.B. We don't use retry / attempts here, since this doesn't involve any RPC.
    130         CommandResult res = getRunUtil()
    131                 .runTimedCmd(mTimeoutMs, GSUTIL, CMD_VERSION);
    132 
    133         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
    134             throw new IOException(
    135                     "gsutil is not installed.\n"
    136                             + "https://cloud.google.com/storage/docs/gsutil for instructions.");
    137         }
    138 
    139         mCheckedGsutil = true;
    140     }
    141 
    142     /**
    143      * Copy a file or directory to or from the bucket.
    144      *
    145      * @param source Source file or pattern
    146      * @param dest Destination file or pattern
    147      * @return {@link CommandResult} result of the operation.
    148      */
    149     public CommandResult copy(String source, String dest) throws IOException {
    150         checkGSUtil();
    151         CLog.d("Copying %s => %s", source, dest);
    152 
    153         IRunUtil run = getRunUtil();
    154         List<String> command = new ArrayList<>();
    155 
    156         command.add(GSUTIL);
    157 
    158         if (mParallel) {
    159             command.add(FLAG_PARALLEL);
    160         }
    161 
    162         command.add(CMD_COPY);
    163 
    164         if (mRecursive) {
    165             command.add(FLAG_RECURSIVE);
    166         }
    167 
    168         if (mNoClobber) {
    169             command.add(FLAG_NO_CLOBBER);
    170         }
    171 
    172         command.add(source);
    173         command.add(dest);
    174 
    175         String[] commandAsStr = command.toArray(new String[0]);
    176 
    177         CommandResult res = run
    178                 .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts, commandAsStr);
    179         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
    180             throw new IOException(
    181                     String.format(
    182                             "Failed to copy '%s' -> '%s' with %s\nstdout: %s\nstderr: %s",
    183                             source,
    184                             dest,
    185                             res.getStatus(),
    186                             res.getStdout(),
    187                             res.getStderr()));
    188         }
    189         return res;
    190     }
    191 
    192     public int getAttempts() {
    193         return mAttempts;
    194     }
    195 
    196     public String getBotoConfig() {
    197         return mBotoConfig;
    198     }
    199 
    200     public String getBotoPath() {
    201         return mBotoPath;
    202     }
    203 
    204     public String getBucketName() {
    205         return mBucketName;
    206     }
    207 
    208     public boolean getNoClobber() {
    209         return mNoClobber;
    210     }
    211 
    212     public boolean getParallel() {
    213         return mParallel;
    214     }
    215 
    216     public boolean getRecursive() {
    217         return mRecursive;
    218     }
    219 
    220     public long getRetryInterval() {
    221         return mRetryInterval;
    222     }
    223 
    224     protected IRunUtil getRunUtil() {
    225         IRunUtil run = new RunUtil();
    226 
    227         if (mBotoPath != null) {
    228             run.setEnvVariable(ENV_BOTO_PATH, mBotoPath);
    229         }
    230 
    231         if (mBotoConfig != null) {
    232             run.setEnvVariable(ENV_BOTO_CONFIG, mBotoConfig);
    233         }
    234 
    235         return run;
    236     }
    237 
    238     public long getTimeout() {
    239         return mTimeoutMs;
    240     }
    241 
    242     /**
    243      * Retrieve the gs://bucket/path URI
    244      */
    245     String getUriForGcsPath(Path path) {
    246         // N.B. Would just use java.net.URI, but it doesn't allow e.g. underscores,
    247         // which are valid in GCS bucket names.
    248         if (!path.isAbsolute()) {
    249             path = Paths.get("/").resolve(path);
    250         }
    251         return String.format("%s://%s%s", GCS_SCHEME, mBucketName, path.toString());
    252     }
    253 
    254     /**
    255      * Make the GCS bucket.
    256      *
    257      * @return {@link CommandResult} result of the operation.
    258      * @throws IOException
    259      */
    260     public CommandResult makeBucket(String projectId) throws IOException {
    261         checkGSUtil();
    262         CLog.d("Making bucket %s for project %s", mBucketName, projectId);
    263 
    264         List<String> command = new ArrayList<>();
    265         command.add(GSUTIL);
    266         command.add(CMD_MAKE_BUCKET);
    267 
    268         if (projectId != null) {
    269             command.add(FLAG_PROJECT_ID);
    270             command.add(projectId);
    271         }
    272 
    273         command.add(getUriForGcsPath(Paths.get("/")));
    274 
    275         CommandResult res = getRunUtil()
    276                 .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts,
    277                         command.toArray(new String[0]));
    278 
    279         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
    280             throw new IOException(
    281                     String.format(
    282                             "Failed to create bucket '%s' with %s\nstdout: %s\nstderr: %s",
    283                             mBucketName,
    284                             res.getStatus(),
    285                             res.getStdout(),
    286                             res.getStderr()));
    287         }
    288 
    289         return res;
    290     }
    291 
    292     /**
    293      * Download a file or directory from a GCS bucket to the current directory.
    294      *
    295      * @param bucketPath File path in the GCS bucket
    296      * @return {@link CommandResult} result of the operation.
    297      */
    298     public CommandResult pull(Path bucketPath) throws IOException {
    299         return copy(getUriForGcsPath(bucketPath), ".");
    300     }
    301 
    302     /**
    303      * Download a file or directory from a GCS bucket.
    304      *
    305      * @param bucketPath File path in the GCS bucket
    306      * @param localFile Local destination path
    307      * @return {@link CommandResult} result of the operation.
    308      */
    309     public CommandResult pull(Path bucketPath, File localFile) throws IOException {
    310         return copy(getUriForGcsPath(bucketPath), localFile.getPath());
    311     }
    312 
    313     /**
    314      * Download a file from a GCS bucket, and extract its contents.
    315      *
    316      * @param bucketPath File path in the GCS bucket
    317      * @return String contents of the file
    318      */
    319     public String pullContents(Path bucketPath) throws IOException {
    320         CommandResult res = copy(getUriForGcsPath(bucketPath), FILENAME_STDOUT);
    321         return res.getStdout();
    322     }
    323 
    324     /**
    325      * Upload a local file or directory to a GCS bucket.
    326      *
    327      * @param localFile Local file or directory
    328      * @return {@link CommandResult} result of the operation.
    329      */
    330     public CommandResult push(File localFile) throws IOException {
    331         return push(localFile, Paths.get("/"));
    332     }
    333 
    334     /**
    335      * Upload a local file or directory to a GCS bucket with a specific path.
    336      *
    337      * @param localFile Local file or directory
    338      * @param bucketPath File path in the GCS bucket
    339      * @return {@link CommandResult} result of the operation.
    340      */
    341     public CommandResult push(File localFile, Path bucketPath) throws IOException {
    342         return copy(localFile.getAbsolutePath(), getUriForGcsPath(bucketPath));
    343     }
    344 
    345     /**
    346      * Upload a String to a GCS bucket.
    347      *
    348      * @param contents File contents, as a string
    349      * @param bucketPath File path in the GCS bucket
    350      * @return {@link CommandResult} result of the operation.
    351      */
    352     public CommandResult pushString(String contents, Path bucketPath) throws IOException {
    353         File localFile = null;
    354         try {
    355             localFile = FileUtil.createTempFile(mBucketName, null);
    356             FileUtil.writeToFile(contents, localFile);
    357             return copy(localFile.getAbsolutePath(), getUriForGcsPath(bucketPath));
    358         } finally {
    359             FileUtil.deleteFile(localFile);
    360         }
    361     }
    362 
    363     /**
    364      * Remove a file or directory from the bucket.
    365      *
    366      * @param pattern File, directory, or pattern to remove.
    367      * @param force Whether to ignore failures and continue silently (will not throw)
    368      */
    369     public CommandResult remove(String pattern, boolean force) throws IOException {
    370         checkGSUtil();
    371         String path = getUriForGcsPath(Paths.get(pattern));
    372         Log.d("Removing file(s) %s", path);
    373 
    374         List<String> command = new ArrayList<>();
    375         command.add(GSUTIL);
    376         command.add(CMD_REMOVE);
    377 
    378         if (mRecursive) {
    379             command.add(FLAG_RECURSIVE);
    380         }
    381 
    382         if (force) {
    383             command.add(FLAG_FORCE);
    384         }
    385 
    386         command.add(path);
    387 
    388         CommandResult res = getRunUtil()
    389                 .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts,
    390                         command.toArray(new String[0]));
    391 
    392         if (!force && !CommandStatus.SUCCESS.equals(res.getStatus())) {
    393             throw new IOException(
    394                     String.format(
    395                             "Failed to remove '%s' with %s\nstdout: %s\nstderr: %s",
    396                             pattern,
    397                             res.getStatus(),
    398                             res.getStdout(),
    399                             res.getStderr()));
    400         }
    401         return res;
    402     }
    403 
    404     /**
    405      * Remove a file or directory from the bucket.
    406      *
    407      * @param pattern File, directory, or pattern to remove.
    408      */
    409     public CommandResult remove(String pattern) throws IOException {
    410         return remove(pattern, false);
    411     }
    412 
    413     /**
    414      * Remove a file or directory from the bucket.
    415      *
    416      * @param path Path to remove
    417      * @param force Whether to fail if the file does not exist
    418      */
    419     public CommandResult remove(Path path, boolean force) throws IOException {
    420         return remove(path.toString(), force);
    421     }
    422 
    423     /**
    424      * Remove a file or directory from the bucket.
    425      *
    426      * @param path Path to remove
    427      */
    428     public CommandResult remove(Path path) throws IOException {
    429         return remove(path.toString(), false);
    430     }
    431 
    432 
    433     /**
    434      * Remove the GCS bucket
    435      *
    436      * @throws IOException
    437      */
    438     public CommandResult removeBucket() throws IOException {
    439         checkGSUtil();
    440         Log.d("Removing bucket %s", mBucketName);
    441 
    442         String[] command = {
    443                 GSUTIL,
    444                 CMD_REMOVE_BUCKET,
    445                 getUriForGcsPath(Paths.get("/"))
    446         };
    447 
    448         CommandResult res = getRunUtil()
    449                 .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts, command);
    450 
    451         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
    452             throw new IOException(
    453                     String.format(
    454                             "Failed to remove bucket '%s' with %s\nstdout: %s\nstderr: %s",
    455                             mBucketName,
    456                             res.getStatus(),
    457                             res.getStdout(),
    458                             res.getStderr()));
    459         }
    460 
    461         return res;
    462     }
    463 
    464     public void setAttempts(int attempts) {
    465         mAttempts = attempts;
    466     }
    467 
    468     public void setBotoConfig(String botoConfig) {
    469         mBotoConfig = botoConfig;
    470     }
    471 
    472     public void setBotoPath(String botoPath) {
    473         mBotoPath = botoPath;
    474     }
    475 
    476     public void setBucketName(String bucketName) {
    477         mBucketName = bucketName;
    478     }
    479 
    480     public void setNoClobber(boolean noClobber) {
    481         mNoClobber = noClobber;
    482     }
    483 
    484     public void setParallel(boolean parallel) {
    485         mParallel = parallel;
    486     }
    487 
    488     public void setRecursive(boolean recursive) {
    489         mRecursive = recursive;
    490     }
    491 
    492     public void setRetryInterval(long retryInterval) {
    493         mRetryInterval = retryInterval;
    494     }
    495 
    496     public void setTimeoutMs(long timeout) {
    497         mTimeoutMs = timeout;
    498     }
    499 
    500     public void setTimeout(long timeout, TimeUnit unit) {
    501         setTimeoutMs(unit.toMillis(timeout));
    502     }
    503 
    504 }
    505