blob: 085427497c9f590e4668050db5a95bc3c4971431 [file] [log] [blame]
Zach Riggle508ce432018-03-13 03:53:10 -05001/*
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
17package com.android.tradefed.util;
18
Zach Riggle508ce432018-03-13 03:53:10 -050019import com.android.tradefed.log.LogUtil.CLog;
20
21import java.io.File;
22import java.io.IOException;
23import java.nio.file.Path;
24import java.nio.file.Paths;
25import java.util.ArrayList;
xingdai1adb0d02018-08-01 21:20:18 -070026import java.util.Arrays;
Zach Riggle508ce432018-03-13 03:53:10 -050027import java.util.List;
28import java.util.concurrent.TimeUnit;
xingdai1432ffe2018-08-24 14:44:11 -070029import java.util.regex.Matcher;
30import java.util.regex.Pattern;
Zach Riggle508ce432018-03-13 03:53:10 -050031
32/**
33 * File manager to download and upload files from Google Cloud Storage (GCS).
34 *
Xing Dai842a1762019-01-02 12:13:10 -080035 * <p>This class should NOT be used from the scope of a test (i.e., IRemoteTest).
Zach Riggle508ce432018-03-13 03:53:10 -050036 */
Xing Dai842a1762019-01-02 12:13:10 -080037@Deprecated
Zach Riggle508ce432018-03-13 03:53:10 -050038public class GCSBucketUtil {
39
40 // https://cloud.google.com/storage/docs/gsutil
41
42 private static final String CMD_COPY = "cp";
43 private static final String CMD_MAKE_BUCKET = "mb";
xingdai1adb0d02018-08-01 21:20:18 -070044 private static final String CMD_LS = "ls";
xingdai1432ffe2018-08-24 14:44:11 -070045 private static final String CMD_STAT = "stat";
46 private static final String CMD_HASH = "hash";
Zach Riggle508ce432018-03-13 03:53:10 -050047 private static final String CMD_REMOVE = "rm";
48 private static final String CMD_REMOVE_BUCKET = "rb";
49 private static final String CMD_VERSION = "-v";
50 private static final String ENV_BOTO_PATH = "BOTO_PATH";
51 private static final String ENV_BOTO_CONFIG = "BOTO_CONFIG";
52 private static final String FILENAME_STDOUT = "-";
53 private static final String FLAG_FORCE = "-f";
54 private static final String FLAG_NO_CLOBBER = "-n";
55 private static final String FLAG_PARALLEL = "-m";
56 private static final String FLAG_PROJECT_ID = "-p";
57 private static final String FLAG_RECURSIVE = "-r";
58 private static final String GCS_SCHEME = "gs";
59 private static final String GSUTIL = "gsutil";
60
61 /**
62 * Whether gsutil is verified to be installed
63 */
64 private static boolean mCheckedGsutil = false;
65
66 /**
67 * Number of attempts for gsutil operations.
68 *
69 * @see RunUtil#runTimedCmdRetry
70 */
71 private int mAttempts = 1;
72
73 /**
74 * Path to the .boto files to use, set via environment variable $BOTO_PATH.
75 *
76 * @see <a href="https://cloud.google.com/storage/docs/gsutil/commands/config">
77 * gsutil documentation</a>
78 */
79 private String mBotoPath = null;
80
81 /**
82 * Path to the .boto file to use, set via environment variable $BOTO_CONFIG.
83 *
84 * @see <a href="https://cloud.google.com/storage/docs/gsutil/commands/config">
85 * gsutil documentation</a>
86 */
87 private String mBotoConfig = null;
88
89 /**
90 * Name of the GCS bucket.
91 */
92 private String mBucketName = null;
93
94 /**
95 * Whether to use the "-n" flag to avoid clobbering files.
96 */
97 private boolean mNoClobber = false;
98
99 /**
100 * Whether to use the "-m" flag to parallelize large operations.
101 */
102 private boolean mParallel = false;
103
104 /**
105 * Whether to use the "-r" flag to perform a recursive copy.
106 */
107 private boolean mRecursive = true;
108
109 /**
110 * Retry interval for gsutil operations.
111 *
112 * @see RunUtil#runTimedCmdRetry
113 */
114 private long mRetryInterval = 0;
115
116 /**
117 * Timeout for gsutil operations.
118 *
119 * @see RunUtil#runTimedCmdRetry
120 */
121 private long mTimeoutMs = 0;
122
123 public GCSBucketUtil(String bucketName) {
124 setBucketName(bucketName);
125 }
126
127 /**
128 * Verify that gsutil is installed.
129 */
130 void checkGSUtil() throws IOException {
131 if (mCheckedGsutil) {
132 return;
133 }
134
135 // N.B. We don't use retry / attempts here, since this doesn't involve any RPC.
136 CommandResult res = getRunUtil()
137 .runTimedCmd(mTimeoutMs, GSUTIL, CMD_VERSION);
138
139 if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
140 throw new IOException(
141 "gsutil is not installed.\n"
142 + "https://cloud.google.com/storage/docs/gsutil for instructions.");
143 }
144
145 mCheckedGsutil = true;
146 }
147
148 /**
149 * Copy a file or directory to or from the bucket.
150 *
151 * @param source Source file or pattern
152 * @param dest Destination file or pattern
153 * @return {@link CommandResult} result of the operation.
154 */
155 public CommandResult copy(String source, String dest) throws IOException {
156 checkGSUtil();
157 CLog.d("Copying %s => %s", source, dest);
158
159 IRunUtil run = getRunUtil();
160 List<String> command = new ArrayList<>();
161
162 command.add(GSUTIL);
163
164 if (mParallel) {
165 command.add(FLAG_PARALLEL);
166 }
167
168 command.add(CMD_COPY);
169
170 if (mRecursive) {
171 command.add(FLAG_RECURSIVE);
172 }
173
174 if (mNoClobber) {
175 command.add(FLAG_NO_CLOBBER);
176 }
177
178 command.add(source);
179 command.add(dest);
180
181 String[] commandAsStr = command.toArray(new String[0]);
182
183 CommandResult res = run
184 .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts, commandAsStr);
185 if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
186 throw new IOException(
187 String.format(
188 "Failed to copy '%s' -> '%s' with %s\nstdout: %s\nstderr: %s",
189 source,
190 dest,
191 res.getStatus(),
192 res.getStdout(),
193 res.getStderr()));
194 }
195 return res;
196 }
197
198 public int getAttempts() {
199 return mAttempts;
200 }
201
202 public String getBotoConfig() {
203 return mBotoConfig;
204 }
205
206 public String getBotoPath() {
207 return mBotoPath;
208 }
209
210 public String getBucketName() {
211 return mBucketName;
212 }
213
214 public boolean getNoClobber() {
215 return mNoClobber;
216 }
217
218 public boolean getParallel() {
219 return mParallel;
220 }
221
222 public boolean getRecursive() {
223 return mRecursive;
224 }
225
226 public long getRetryInterval() {
227 return mRetryInterval;
228 }
229
230 protected IRunUtil getRunUtil() {
231 IRunUtil run = new RunUtil();
232
233 if (mBotoPath != null) {
234 run.setEnvVariable(ENV_BOTO_PATH, mBotoPath);
235 }
236
237 if (mBotoConfig != null) {
238 run.setEnvVariable(ENV_BOTO_CONFIG, mBotoConfig);
239 }
240
241 return run;
242 }
243
244 public long getTimeout() {
245 return mTimeoutMs;
246 }
247
248 /**
249 * Retrieve the gs://bucket/path URI
250 */
251 String getUriForGcsPath(Path path) {
252 // N.B. Would just use java.net.URI, but it doesn't allow e.g. underscores,
253 // which are valid in GCS bucket names.
254 if (!path.isAbsolute()) {
255 path = Paths.get("/").resolve(path);
256 }
257 return String.format("%s://%s%s", GCS_SCHEME, mBucketName, path.toString());
258 }
259
260 /**
261 * Make the GCS bucket.
262 *
263 * @return {@link CommandResult} result of the operation.
264 * @throws IOException
265 */
266 public CommandResult makeBucket(String projectId) throws IOException {
267 checkGSUtil();
268 CLog.d("Making bucket %s for project %s", mBucketName, projectId);
269
270 List<String> command = new ArrayList<>();
271 command.add(GSUTIL);
272 command.add(CMD_MAKE_BUCKET);
273
274 if (projectId != null) {
275 command.add(FLAG_PROJECT_ID);
276 command.add(projectId);
277 }
278
279 command.add(getUriForGcsPath(Paths.get("/")));
280
281 CommandResult res = getRunUtil()
282 .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts,
283 command.toArray(new String[0]));
284
285 if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
286 throw new IOException(
287 String.format(
288 "Failed to create bucket '%s' with %s\nstdout: %s\nstderr: %s",
289 mBucketName,
290 res.getStatus(),
291 res.getStdout(),
292 res.getStderr()));
293 }
294
295 return res;
296 }
297
298 /**
xingdai1adb0d02018-08-01 21:20:18 -0700299 * List files under a GCS path.
300 *
301 * @param bucketPath the GCS path
302 * @return a list of {@link String}s that are files under the GCS path
303 * @throws IOException
304 */
305 public List<String> ls(Path bucketPath) throws IOException {
306 checkGSUtil();
307 CLog.d("Check stat of %s %s", mBucketName, bucketPath);
308
309 List<String> command = new ArrayList<>();
310 command.add(GSUTIL);
311 command.add(CMD_LS);
312
313 command.add(getUriForGcsPath(bucketPath));
314
315 CommandResult res =
316 getRunUtil()
317 .runTimedCmdRetry(
318 mTimeoutMs,
319 mRetryInterval,
320 mAttempts,
321 command.toArray(new String[0]));
322
323 if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
324 throw new IOException(
325 String.format(
326 "Failed to list path '%s %s' with %s\nstdout: %s\nstderr: %s",
327 mBucketName,
328 bucketPath,
329 res.getStatus(),
330 res.getStdout(),
331 res.getStderr()));
332 }
333 return Arrays.asList(res.getStdout().split("\n"));
334 }
335
336 /**
337 * Check a GCS file is a file or not a file (a folder).
338 *
339 * <p>If the filename ends with '/', then it's a folder. gsutil ls gs://filename should return
340 * the gs://filename if it's a file. gsutil ls gs://folder name should return the files in the
341 * folder if there are files in the folder. And it will return gs://folder/ if there is no files
342 * in the folder.
343 *
344 * @param path the path relative to bucket..
345 * @return it's a file or not a file.
346 * @throws IOException
347 */
348 public boolean isFile(String path) throws IOException {
349 if (path.endsWith("/")) {
350 return false;
351 }
352 List<String> files = ls(Paths.get(path));
353 if (files.size() > 1) {
354 return false;
355 }
356 if (files.size() == 1) {
357 return files.get(0).equals(getUriForGcsPath(Paths.get(path)));
358 }
359 return false;
360 }
361
xingdai1432ffe2018-08-24 14:44:11 -0700362 /** Simple wrapper for file info in GCS. */
363 public static class GCSFileMetadata {
364 public String mName;
365 public String mMd5Hash = null;
366
367 private GCSFileMetadata() {}
368
369 /**
370 * Parse a string to a {@link GCSFileMetadata} object.
371 *
372 * @param statOutput
373 * @return {@link GCSFileMetadata}
374 */
375 public static GCSFileMetadata parseStat(String statOutput) {
376 GCSFileMetadata info = new GCSFileMetadata();
377 String[] infoLines = statOutput.split("\n");
378 // Remove the trail ':'
379 info.mName = infoLines[0].substring(0, infoLines[0].length() - 1);
380 for (String line : infoLines) {
381 String[] keyValue = line.split(":", 2);
382 String key = keyValue[0].trim();
383 String value = keyValue[1].trim();
384
385 if ("Hash (md5)".equals(key)) {
386 info.mMd5Hash = value;
387 }
388 }
389 return info;
390 }
391 }
392
393 /**
394 * Get the state of the file for the GCS path.
395 *
396 * @param bucketPath the GCS path
397 * @return {@link GCSFileMetadata} for the GCS path
398 * @throws IOException
399 */
400 public GCSFileMetadata stat(Path bucketPath) throws IOException {
401 checkGSUtil();
402 CLog.d("Check stat of %s %s", mBucketName, bucketPath);
403
404 List<String> command = new ArrayList<>();
405 command.add(GSUTIL);
406 command.add(CMD_STAT);
407
408 command.add(getUriForGcsPath(bucketPath));
409
410 // The stat output will be something like:
411 // gs://bucketName/file.txt:
412 // Creation time: Tue, 14 Aug 2018 00:20:48 GMT
413 // Update time: Tue, 14 Aug 2018 16:58:39 GMT
414 // Storage class: STANDARD
415 // Content-Length: 1097
416 // Content-Type: text/x-sh
417 // Hash (crc32c): WutM7Q==
418 // Hash (md5): GZX0xHUXtGnoKIGTDk6Pbg==
419 // ETag: CKKNu/Si69wCEAU=
420 // Generation: 1534206048913058
421 // Metageneration: 5
422 CommandResult res =
423 getRunUtil()
424 .runTimedCmdRetry(
425 mTimeoutMs,
426 mRetryInterval,
427 mAttempts,
428 command.toArray(new String[0]));
429
430 if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
431 throw new IOException(
432 String.format(
433 "Failed to stat path '%s %s' with %s\nstdout: %s\nstderr: %s",
434 mBucketName,
435 bucketPath,
436 res.getStatus(),
437 res.getStdout(),
438 res.getStderr()));
439 }
440 return GCSFileMetadata.parseStat(res.getStdout());
441 }
442
443 /**
444 * Calculate the md5 hash for the local file.
445 *
446 * @param localFile a local file
447 * @return the md5 hash for the local file.
448 * @throws IOException
449 */
450 public String md5Hash(File localFile) throws IOException {
451 checkGSUtil();
452 List<String> command = new ArrayList<>();
453 command.add(GSUTIL);
454 command.add(CMD_HASH);
455 command.add("-m");
456 command.add(localFile.getAbsolutePath());
457
458 CommandResult res =
459 getRunUtil()
460 .runTimedCmdRetry(
461 mTimeoutMs,
462 mRetryInterval,
463 mAttempts,
464 command.toArray(new String[0]));
465
466 if (CommandStatus.SUCCESS.equals(res.getStatus())) {
467 // An example output of "gustil hash -m file":
468 // Hashes [base64] for error_prone_rules.mk:
469 // Hash (md5): eHfvTtNyH/x3GcyfApEIDQ==
470 //
471 // Operation completed over 1 objects/2.0 KiB.
472 Pattern md5Pattern =
473 Pattern.compile(
474 ".*Hash\\s*\\(md5\\)\\:\\s*(.*?)\n.*",
475 Pattern.MULTILINE | Pattern.DOTALL);
476 Matcher matcher = md5Pattern.matcher(res.getStdout());
477 if (matcher.find()) {
478 return matcher.group(1);
479 }
480 }
481 throw new IOException(
482 String.format(
483 "Failed to calculate md5 hash for '%s' with %s\nstdout: %s\nstderr: %s",
484 localFile.getAbsoluteFile(),
485 res.getStatus(),
486 res.getStdout(),
487 res.getStderr()));
488 }
489
xingdai1adb0d02018-08-01 21:20:18 -0700490 /**
Zach Riggle508ce432018-03-13 03:53:10 -0500491 * Download a file or directory from a GCS bucket to the current directory.
492 *
493 * @param bucketPath File path in the GCS bucket
494 * @return {@link CommandResult} result of the operation.
495 */
496 public CommandResult pull(Path bucketPath) throws IOException {
497 return copy(getUriForGcsPath(bucketPath), ".");
498 }
499
500 /**
501 * Download a file or directory from a GCS bucket.
502 *
503 * @param bucketPath File path in the GCS bucket
504 * @param localFile Local destination path
505 * @return {@link CommandResult} result of the operation.
506 */
507 public CommandResult pull(Path bucketPath, File localFile) throws IOException {
508 return copy(getUriForGcsPath(bucketPath), localFile.getPath());
509 }
510
511 /**
512 * Download a file from a GCS bucket, and extract its contents.
513 *
514 * @param bucketPath File path in the GCS bucket
515 * @return String contents of the file
516 */
517 public String pullContents(Path bucketPath) throws IOException {
518 CommandResult res = copy(getUriForGcsPath(bucketPath), FILENAME_STDOUT);
519 return res.getStdout();
520 }
521
522 /**
523 * Upload a local file or directory to a GCS bucket.
524 *
525 * @param localFile Local file or directory
526 * @return {@link CommandResult} result of the operation.
527 */
528 public CommandResult push(File localFile) throws IOException {
529 return push(localFile, Paths.get("/"));
530 }
531
532 /**
533 * Upload a local file or directory to a GCS bucket with a specific path.
534 *
535 * @param localFile Local file or directory
536 * @param bucketPath File path in the GCS bucket
537 * @return {@link CommandResult} result of the operation.
538 */
539 public CommandResult push(File localFile, Path bucketPath) throws IOException {
540 return copy(localFile.getAbsolutePath(), getUriForGcsPath(bucketPath));
541 }
542
543 /**
544 * Upload a String to a GCS bucket.
545 *
546 * @param contents File contents, as a string
547 * @param bucketPath File path in the GCS bucket
548 * @return {@link CommandResult} result of the operation.
549 */
550 public CommandResult pushString(String contents, Path bucketPath) throws IOException {
551 File localFile = null;
552 try {
553 localFile = FileUtil.createTempFile(mBucketName, null);
554 FileUtil.writeToFile(contents, localFile);
555 return copy(localFile.getAbsolutePath(), getUriForGcsPath(bucketPath));
556 } finally {
557 FileUtil.deleteFile(localFile);
558 }
559 }
560
561 /**
562 * Remove a file or directory from the bucket.
563 *
564 * @param pattern File, directory, or pattern to remove.
565 * @param force Whether to ignore failures and continue silently (will not throw)
566 */
567 public CommandResult remove(String pattern, boolean force) throws IOException {
568 checkGSUtil();
569 String path = getUriForGcsPath(Paths.get(pattern));
xingdai1432ffe2018-08-24 14:44:11 -0700570 CLog.d("Removing file(s) %s", path);
Zach Riggle508ce432018-03-13 03:53:10 -0500571
572 List<String> command = new ArrayList<>();
573 command.add(GSUTIL);
574 command.add(CMD_REMOVE);
575
576 if (mRecursive) {
577 command.add(FLAG_RECURSIVE);
578 }
579
580 if (force) {
581 command.add(FLAG_FORCE);
582 }
583
584 command.add(path);
585
586 CommandResult res = getRunUtil()
587 .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts,
588 command.toArray(new String[0]));
589
590 if (!force && !CommandStatus.SUCCESS.equals(res.getStatus())) {
591 throw new IOException(
592 String.format(
593 "Failed to remove '%s' with %s\nstdout: %s\nstderr: %s",
594 pattern,
595 res.getStatus(),
596 res.getStdout(),
597 res.getStderr()));
598 }
599 return res;
600 }
601
602 /**
603 * Remove a file or directory from the bucket.
604 *
605 * @param pattern File, directory, or pattern to remove.
606 */
607 public CommandResult remove(String pattern) throws IOException {
608 return remove(pattern, false);
609 }
610
611 /**
612 * Remove a file or directory from the bucket.
613 *
614 * @param path Path to remove
615 * @param force Whether to fail if the file does not exist
616 */
617 public CommandResult remove(Path path, boolean force) throws IOException {
618 return remove(path.toString(), force);
619 }
620
621 /**
622 * Remove a file or directory from the bucket.
623 *
624 * @param path Path to remove
625 */
626 public CommandResult remove(Path path) throws IOException {
627 return remove(path.toString(), false);
628 }
629
630
631 /**
632 * Remove the GCS bucket
633 *
634 * @throws IOException
635 */
636 public CommandResult removeBucket() throws IOException {
637 checkGSUtil();
xingdai1432ffe2018-08-24 14:44:11 -0700638 CLog.d("Removing bucket %s", mBucketName);
Zach Riggle508ce432018-03-13 03:53:10 -0500639
640 String[] command = {
641 GSUTIL,
642 CMD_REMOVE_BUCKET,
643 getUriForGcsPath(Paths.get("/"))
644 };
645
646 CommandResult res = getRunUtil()
647 .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts, command);
648
649 if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
650 throw new IOException(
651 String.format(
652 "Failed to remove bucket '%s' with %s\nstdout: %s\nstderr: %s",
653 mBucketName,
654 res.getStatus(),
655 res.getStdout(),
656 res.getStderr()));
657 }
658
659 return res;
660 }
661
662 public void setAttempts(int attempts) {
663 mAttempts = attempts;
664 }
665
666 public void setBotoConfig(String botoConfig) {
667 mBotoConfig = botoConfig;
668 }
669
670 public void setBotoPath(String botoPath) {
671 mBotoPath = botoPath;
672 }
673
674 public void setBucketName(String bucketName) {
675 mBucketName = bucketName;
676 }
677
678 public void setNoClobber(boolean noClobber) {
679 mNoClobber = noClobber;
680 }
681
682 public void setParallel(boolean parallel) {
683 mParallel = parallel;
684 }
685
686 public void setRecursive(boolean recursive) {
687 mRecursive = recursive;
688 }
689
690 public void setRetryInterval(long retryInterval) {
691 mRetryInterval = retryInterval;
692 }
693
694 public void setTimeoutMs(long timeout) {
695 mTimeoutMs = timeout;
696 }
697
698 public void setTimeout(long timeout, TimeUnit unit) {
699 setTimeoutMs(unit.toMillis(timeout));
700 }
Zach Riggle508ce432018-03-13 03:53:10 -0500701}