| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tradefed.util; |
| |
| import com.android.tradefed.log.LogUtil.CLog; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * File manager to download and upload files from Google Cloud Storage (GCS). |
| * |
| * <p>This class should NOT be used from the scope of a test (i.e., IRemoteTest). |
| */ |
| @Deprecated |
| public class GCSBucketUtil { |
| |
| // https://cloud.google.com/storage/docs/gsutil |
| |
| private static final String CMD_COPY = "cp"; |
| private static final String CMD_MAKE_BUCKET = "mb"; |
| private static final String CMD_LS = "ls"; |
| private static final String CMD_STAT = "stat"; |
| private static final String CMD_HASH = "hash"; |
| private static final String CMD_REMOVE = "rm"; |
| private static final String CMD_REMOVE_BUCKET = "rb"; |
| private static final String CMD_VERSION = "-v"; |
| private static final String ENV_BOTO_PATH = "BOTO_PATH"; |
| private static final String ENV_BOTO_CONFIG = "BOTO_CONFIG"; |
| private static final String FILENAME_STDOUT = "-"; |
| private static final String FLAG_FORCE = "-f"; |
| private static final String FLAG_NO_CLOBBER = "-n"; |
| private static final String FLAG_PARALLEL = "-m"; |
| private static final String FLAG_PROJECT_ID = "-p"; |
| private static final String FLAG_RECURSIVE = "-r"; |
| private static final String GCS_SCHEME = "gs"; |
| private static final String GSUTIL = "gsutil"; |
| |
| /** |
| * Whether gsutil is verified to be installed |
| */ |
| private static boolean mCheckedGsutil = false; |
| |
| /** |
| * Number of attempts for gsutil operations. |
| * |
| * @see RunUtil#runTimedCmdRetry |
| */ |
| private int mAttempts = 1; |
| |
| /** |
| * Path to the .boto files to use, set via environment variable $BOTO_PATH. |
| * |
| * @see <a href="https://cloud.google.com/storage/docs/gsutil/commands/config"> |
| * gsutil documentation</a> |
| */ |
| private String mBotoPath = null; |
| |
| /** |
| * Path to the .boto file to use, set via environment variable $BOTO_CONFIG. |
| * |
| * @see <a href="https://cloud.google.com/storage/docs/gsutil/commands/config"> |
| * gsutil documentation</a> |
| */ |
| private String mBotoConfig = null; |
| |
| /** |
| * Name of the GCS bucket. |
| */ |
| private String mBucketName = null; |
| |
| /** |
| * Whether to use the "-n" flag to avoid clobbering files. |
| */ |
| private boolean mNoClobber = false; |
| |
| /** |
| * Whether to use the "-m" flag to parallelize large operations. |
| */ |
| private boolean mParallel = false; |
| |
| /** |
| * Whether to use the "-r" flag to perform a recursive copy. |
| */ |
| private boolean mRecursive = true; |
| |
| /** |
| * Retry interval for gsutil operations. |
| * |
| * @see RunUtil#runTimedCmdRetry |
| */ |
| private long mRetryInterval = 0; |
| |
| /** |
| * Timeout for gsutil operations. |
| * |
| * @see RunUtil#runTimedCmdRetry |
| */ |
| private long mTimeoutMs = 0; |
| |
| public GCSBucketUtil(String bucketName) { |
| setBucketName(bucketName); |
| } |
| |
| /** |
| * Verify that gsutil is installed. |
| */ |
| void checkGSUtil() throws IOException { |
| if (mCheckedGsutil) { |
| return; |
| } |
| |
| // N.B. We don't use retry / attempts here, since this doesn't involve any RPC. |
| CommandResult res = getRunUtil() |
| .runTimedCmd(mTimeoutMs, GSUTIL, CMD_VERSION); |
| |
| if (!CommandStatus.SUCCESS.equals(res.getStatus())) { |
| throw new IOException( |
| "gsutil is not installed.\n" |
| + "https://cloud.google.com/storage/docs/gsutil for instructions."); |
| } |
| |
| mCheckedGsutil = true; |
| } |
| |
| /** |
| * Copy a file or directory to or from the bucket. |
| * |
| * @param source Source file or pattern |
| * @param dest Destination file or pattern |
| * @return {@link CommandResult} result of the operation. |
| */ |
| public CommandResult copy(String source, String dest) throws IOException { |
| checkGSUtil(); |
| CLog.d("Copying %s => %s", source, dest); |
| |
| IRunUtil run = getRunUtil(); |
| List<String> command = new ArrayList<>(); |
| |
| command.add(GSUTIL); |
| |
| if (mParallel) { |
| command.add(FLAG_PARALLEL); |
| } |
| |
| command.add(CMD_COPY); |
| |
| if (mRecursive) { |
| command.add(FLAG_RECURSIVE); |
| } |
| |
| if (mNoClobber) { |
| command.add(FLAG_NO_CLOBBER); |
| } |
| |
| command.add(source); |
| command.add(dest); |
| |
| String[] commandAsStr = command.toArray(new String[0]); |
| |
| CommandResult res = run |
| .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts, commandAsStr); |
| if (!CommandStatus.SUCCESS.equals(res.getStatus())) { |
| throw new IOException( |
| String.format( |
| "Failed to copy '%s' -> '%s' with %s\nstdout: %s\nstderr: %s", |
| source, |
| dest, |
| res.getStatus(), |
| res.getStdout(), |
| res.getStderr())); |
| } |
| return res; |
| } |
| |
| public int getAttempts() { |
| return mAttempts; |
| } |
| |
| public String getBotoConfig() { |
| return mBotoConfig; |
| } |
| |
| public String getBotoPath() { |
| return mBotoPath; |
| } |
| |
| public String getBucketName() { |
| return mBucketName; |
| } |
| |
| public boolean getNoClobber() { |
| return mNoClobber; |
| } |
| |
| public boolean getParallel() { |
| return mParallel; |
| } |
| |
| public boolean getRecursive() { |
| return mRecursive; |
| } |
| |
| public long getRetryInterval() { |
| return mRetryInterval; |
| } |
| |
| protected IRunUtil getRunUtil() { |
| IRunUtil run = new RunUtil(); |
| |
| if (mBotoPath != null) { |
| run.setEnvVariable(ENV_BOTO_PATH, mBotoPath); |
| } |
| |
| if (mBotoConfig != null) { |
| run.setEnvVariable(ENV_BOTO_CONFIG, mBotoConfig); |
| } |
| |
| return run; |
| } |
| |
| public long getTimeout() { |
| return mTimeoutMs; |
| } |
| |
| /** |
| * Retrieve the gs://bucket/path URI |
| */ |
| String getUriForGcsPath(Path path) { |
| // N.B. Would just use java.net.URI, but it doesn't allow e.g. underscores, |
| // which are valid in GCS bucket names. |
| if (!path.isAbsolute()) { |
| path = Paths.get("/").resolve(path); |
| } |
| return String.format("%s://%s%s", GCS_SCHEME, mBucketName, path.toString()); |
| } |
| |
| /** |
| * Make the GCS bucket. |
| * |
| * @return {@link CommandResult} result of the operation. |
| * @throws IOException |
| */ |
| public CommandResult makeBucket(String projectId) throws IOException { |
| checkGSUtil(); |
| CLog.d("Making bucket %s for project %s", mBucketName, projectId); |
| |
| List<String> command = new ArrayList<>(); |
| command.add(GSUTIL); |
| command.add(CMD_MAKE_BUCKET); |
| |
| if (projectId != null) { |
| command.add(FLAG_PROJECT_ID); |
| command.add(projectId); |
| } |
| |
| command.add(getUriForGcsPath(Paths.get("/"))); |
| |
| CommandResult res = getRunUtil() |
| .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts, |
| command.toArray(new String[0])); |
| |
| if (!CommandStatus.SUCCESS.equals(res.getStatus())) { |
| throw new IOException( |
| String.format( |
| "Failed to create bucket '%s' with %s\nstdout: %s\nstderr: %s", |
| mBucketName, |
| res.getStatus(), |
| res.getStdout(), |
| res.getStderr())); |
| } |
| |
| return res; |
| } |
| |
| /** |
| * List files under a GCS path. |
| * |
| * @param bucketPath the GCS path |
| * @return a list of {@link String}s that are files under the GCS path |
| * @throws IOException |
| */ |
| public List<String> ls(Path bucketPath) throws IOException { |
| checkGSUtil(); |
| CLog.d("Check stat of %s %s", mBucketName, bucketPath); |
| |
| List<String> command = new ArrayList<>(); |
| command.add(GSUTIL); |
| command.add(CMD_LS); |
| |
| command.add(getUriForGcsPath(bucketPath)); |
| |
| CommandResult res = |
| getRunUtil() |
| .runTimedCmdRetry( |
| mTimeoutMs, |
| mRetryInterval, |
| mAttempts, |
| command.toArray(new String[0])); |
| |
| if (!CommandStatus.SUCCESS.equals(res.getStatus())) { |
| throw new IOException( |
| String.format( |
| "Failed to list path '%s %s' with %s\nstdout: %s\nstderr: %s", |
| mBucketName, |
| bucketPath, |
| res.getStatus(), |
| res.getStdout(), |
| res.getStderr())); |
| } |
| return Arrays.asList(res.getStdout().split("\n")); |
| } |
| |
| /** |
| * Check a GCS file is a file or not a file (a folder). |
| * |
| * <p>If the filename ends with '/', then it's a folder. gsutil ls gs://filename should return |
| * the gs://filename if it's a file. gsutil ls gs://folder name should return the files in the |
| * folder if there are files in the folder. And it will return gs://folder/ if there is no files |
| * in the folder. |
| * |
| * @param path the path relative to bucket.. |
| * @return it's a file or not a file. |
| * @throws IOException |
| */ |
| public boolean isFile(String path) throws IOException { |
| if (path.endsWith("/")) { |
| return false; |
| } |
| List<String> files = ls(Paths.get(path)); |
| if (files.size() > 1) { |
| return false; |
| } |
| if (files.size() == 1) { |
| return files.get(0).equals(getUriForGcsPath(Paths.get(path))); |
| } |
| return false; |
| } |
| |
| /** Simple wrapper for file info in GCS. */ |
| public static class GCSFileMetadata { |
| public String mName; |
| public String mMd5Hash = null; |
| |
| private GCSFileMetadata() {} |
| |
| /** |
| * Parse a string to a {@link GCSFileMetadata} object. |
| * |
| * @param statOutput |
| * @return {@link GCSFileMetadata} |
| */ |
| public static GCSFileMetadata parseStat(String statOutput) { |
| GCSFileMetadata info = new GCSFileMetadata(); |
| String[] infoLines = statOutput.split("\n"); |
| // Remove the trail ':' |
| info.mName = infoLines[0].substring(0, infoLines[0].length() - 1); |
| for (String line : infoLines) { |
| String[] keyValue = line.split(":", 2); |
| String key = keyValue[0].trim(); |
| String value = keyValue[1].trim(); |
| |
| if ("Hash (md5)".equals(key)) { |
| info.mMd5Hash = value; |
| } |
| } |
| return info; |
| } |
| } |
| |
| /** |
| * Get the state of the file for the GCS path. |
| * |
| * @param bucketPath the GCS path |
| * @return {@link GCSFileMetadata} for the GCS path |
| * @throws IOException |
| */ |
| public GCSFileMetadata stat(Path bucketPath) throws IOException { |
| checkGSUtil(); |
| CLog.d("Check stat of %s %s", mBucketName, bucketPath); |
| |
| List<String> command = new ArrayList<>(); |
| command.add(GSUTIL); |
| command.add(CMD_STAT); |
| |
| command.add(getUriForGcsPath(bucketPath)); |
| |
| // The stat output will be something like: |
| // gs://bucketName/file.txt: |
| // Creation time: Tue, 14 Aug 2018 00:20:48 GMT |
| // Update time: Tue, 14 Aug 2018 16:58:39 GMT |
| // Storage class: STANDARD |
| // Content-Length: 1097 |
| // Content-Type: text/x-sh |
| // Hash (crc32c): WutM7Q== |
| // Hash (md5): GZX0xHUXtGnoKIGTDk6Pbg== |
| // ETag: CKKNu/Si69wCEAU= |
| // Generation: 1534206048913058 |
| // Metageneration: 5 |
| CommandResult res = |
| getRunUtil() |
| .runTimedCmdRetry( |
| mTimeoutMs, |
| mRetryInterval, |
| mAttempts, |
| command.toArray(new String[0])); |
| |
| if (!CommandStatus.SUCCESS.equals(res.getStatus())) { |
| throw new IOException( |
| String.format( |
| "Failed to stat path '%s %s' with %s\nstdout: %s\nstderr: %s", |
| mBucketName, |
| bucketPath, |
| res.getStatus(), |
| res.getStdout(), |
| res.getStderr())); |
| } |
| return GCSFileMetadata.parseStat(res.getStdout()); |
| } |
| |
| /** |
| * Calculate the md5 hash for the local file. |
| * |
| * @param localFile a local file |
| * @return the md5 hash for the local file. |
| * @throws IOException |
| */ |
| public String md5Hash(File localFile) throws IOException { |
| checkGSUtil(); |
| List<String> command = new ArrayList<>(); |
| command.add(GSUTIL); |
| command.add(CMD_HASH); |
| command.add("-m"); |
| command.add(localFile.getAbsolutePath()); |
| |
| CommandResult res = |
| getRunUtil() |
| .runTimedCmdRetry( |
| mTimeoutMs, |
| mRetryInterval, |
| mAttempts, |
| command.toArray(new String[0])); |
| |
| if (CommandStatus.SUCCESS.equals(res.getStatus())) { |
| // An example output of "gustil hash -m file": |
| // Hashes [base64] for error_prone_rules.mk: |
| // Hash (md5): eHfvTtNyH/x3GcyfApEIDQ== |
| // |
| // Operation completed over 1 objects/2.0 KiB. |
| Pattern md5Pattern = |
| Pattern.compile( |
| ".*Hash\\s*\\(md5\\)\\:\\s*(.*?)\n.*", |
| Pattern.MULTILINE | Pattern.DOTALL); |
| Matcher matcher = md5Pattern.matcher(res.getStdout()); |
| if (matcher.find()) { |
| return matcher.group(1); |
| } |
| } |
| throw new IOException( |
| String.format( |
| "Failed to calculate md5 hash for '%s' with %s\nstdout: %s\nstderr: %s", |
| localFile.getAbsoluteFile(), |
| res.getStatus(), |
| res.getStdout(), |
| res.getStderr())); |
| } |
| |
| /** |
| * Download a file or directory from a GCS bucket to the current directory. |
| * |
| * @param bucketPath File path in the GCS bucket |
| * @return {@link CommandResult} result of the operation. |
| */ |
| public CommandResult pull(Path bucketPath) throws IOException { |
| return copy(getUriForGcsPath(bucketPath), "."); |
| } |
| |
| /** |
| * Download a file or directory from a GCS bucket. |
| * |
| * @param bucketPath File path in the GCS bucket |
| * @param localFile Local destination path |
| * @return {@link CommandResult} result of the operation. |
| */ |
| public CommandResult pull(Path bucketPath, File localFile) throws IOException { |
| return copy(getUriForGcsPath(bucketPath), localFile.getPath()); |
| } |
| |
| /** |
| * Download a file from a GCS bucket, and extract its contents. |
| * |
| * @param bucketPath File path in the GCS bucket |
| * @return String contents of the file |
| */ |
| public String pullContents(Path bucketPath) throws IOException { |
| CommandResult res = copy(getUriForGcsPath(bucketPath), FILENAME_STDOUT); |
| return res.getStdout(); |
| } |
| |
| /** |
| * Upload a local file or directory to a GCS bucket. |
| * |
| * @param localFile Local file or directory |
| * @return {@link CommandResult} result of the operation. |
| */ |
| public CommandResult push(File localFile) throws IOException { |
| return push(localFile, Paths.get("/")); |
| } |
| |
| /** |
| * Upload a local file or directory to a GCS bucket with a specific path. |
| * |
| * @param localFile Local file or directory |
| * @param bucketPath File path in the GCS bucket |
| * @return {@link CommandResult} result of the operation. |
| */ |
| public CommandResult push(File localFile, Path bucketPath) throws IOException { |
| return copy(localFile.getAbsolutePath(), getUriForGcsPath(bucketPath)); |
| } |
| |
| /** |
| * Upload a String to a GCS bucket. |
| * |
| * @param contents File contents, as a string |
| * @param bucketPath File path in the GCS bucket |
| * @return {@link CommandResult} result of the operation. |
| */ |
| public CommandResult pushString(String contents, Path bucketPath) throws IOException { |
| File localFile = null; |
| try { |
| localFile = FileUtil.createTempFile(mBucketName, null); |
| FileUtil.writeToFile(contents, localFile); |
| return copy(localFile.getAbsolutePath(), getUriForGcsPath(bucketPath)); |
| } finally { |
| FileUtil.deleteFile(localFile); |
| } |
| } |
| |
| /** |
| * Remove a file or directory from the bucket. |
| * |
| * @param pattern File, directory, or pattern to remove. |
| * @param force Whether to ignore failures and continue silently (will not throw) |
| */ |
| public CommandResult remove(String pattern, boolean force) throws IOException { |
| checkGSUtil(); |
| String path = getUriForGcsPath(Paths.get(pattern)); |
| CLog.d("Removing file(s) %s", path); |
| |
| List<String> command = new ArrayList<>(); |
| command.add(GSUTIL); |
| command.add(CMD_REMOVE); |
| |
| if (mRecursive) { |
| command.add(FLAG_RECURSIVE); |
| } |
| |
| if (force) { |
| command.add(FLAG_FORCE); |
| } |
| |
| command.add(path); |
| |
| CommandResult res = getRunUtil() |
| .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts, |
| command.toArray(new String[0])); |
| |
| if (!force && !CommandStatus.SUCCESS.equals(res.getStatus())) { |
| throw new IOException( |
| String.format( |
| "Failed to remove '%s' with %s\nstdout: %s\nstderr: %s", |
| pattern, |
| res.getStatus(), |
| res.getStdout(), |
| res.getStderr())); |
| } |
| return res; |
| } |
| |
| /** |
| * Remove a file or directory from the bucket. |
| * |
| * @param pattern File, directory, or pattern to remove. |
| */ |
| public CommandResult remove(String pattern) throws IOException { |
| return remove(pattern, false); |
| } |
| |
| /** |
| * Remove a file or directory from the bucket. |
| * |
| * @param path Path to remove |
| * @param force Whether to fail if the file does not exist |
| */ |
| public CommandResult remove(Path path, boolean force) throws IOException { |
| return remove(path.toString(), force); |
| } |
| |
| /** |
| * Remove a file or directory from the bucket. |
| * |
| * @param path Path to remove |
| */ |
| public CommandResult remove(Path path) throws IOException { |
| return remove(path.toString(), false); |
| } |
| |
| |
| /** |
| * Remove the GCS bucket |
| * |
| * @throws IOException |
| */ |
| public CommandResult removeBucket() throws IOException { |
| checkGSUtil(); |
| CLog.d("Removing bucket %s", mBucketName); |
| |
| String[] command = { |
| GSUTIL, |
| CMD_REMOVE_BUCKET, |
| getUriForGcsPath(Paths.get("/")) |
| }; |
| |
| CommandResult res = getRunUtil() |
| .runTimedCmdRetry(mTimeoutMs, mRetryInterval, mAttempts, command); |
| |
| if (!CommandStatus.SUCCESS.equals(res.getStatus())) { |
| throw new IOException( |
| String.format( |
| "Failed to remove bucket '%s' with %s\nstdout: %s\nstderr: %s", |
| mBucketName, |
| res.getStatus(), |
| res.getStdout(), |
| res.getStderr())); |
| } |
| |
| return res; |
| } |
| |
| public void setAttempts(int attempts) { |
| mAttempts = attempts; |
| } |
| |
| public void setBotoConfig(String botoConfig) { |
| mBotoConfig = botoConfig; |
| } |
| |
| public void setBotoPath(String botoPath) { |
| mBotoPath = botoPath; |
| } |
| |
| public void setBucketName(String bucketName) { |
| mBucketName = bucketName; |
| } |
| |
| public void setNoClobber(boolean noClobber) { |
| mNoClobber = noClobber; |
| } |
| |
| public void setParallel(boolean parallel) { |
| mParallel = parallel; |
| } |
| |
| public void setRecursive(boolean recursive) { |
| mRecursive = recursive; |
| } |
| |
| public void setRetryInterval(long retryInterval) { |
| mRetryInterval = retryInterval; |
| } |
| |
| public void setTimeoutMs(long timeout) { |
| mTimeoutMs = timeout; |
| } |
| |
| public void setTimeout(long timeout, TimeUnit unit) { |
| setTimeoutMs(unit.toMillis(timeout)); |
| } |
| } |