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