blob: 085427497c9f590e4668050db5a95bc3c4971431 [file] [log] [blame]
/*
* 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));
}
}