/* * Copyright (c) 2016 EMC Corporation * All Rights Reserved */ package com.emc.storageos.management.backup.util; import java.net.ConnectException; import java.net.URI; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import org.apache.http.auth.AuthenticationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emc.storageos.management.backup.BackupConstants; public class FtpClient implements BackupClient { private static final Logger log = LoggerFactory.getLogger(FtpClient.class); private final String uri; private final String username; private final String password; public FtpClient(String uri, String username, String password) { this.uri = uri; this.username = username; this.password = password; } public ProcessBuilder getBuilder() { boolean isExplicit = startsWithIgnoreCase(uri, BackupConstants.FTPS_URL_PREFIX); ProcessBuilder builder = new ProcessBuilder("curl", "-sSk", "-u", String.format("%s:%s", username, password)); if (!isExplicit) { builder.command().add("--ftp-ssl"); } return builder; } public static boolean isSupported(String url) { return startsWithIgnoreCase(url, BackupConstants.FTPS_URL_PREFIX) || startsWithIgnoreCase(url, BackupConstants.FTP_URL_PREFIX); } private static boolean startsWithIgnoreCase(String str, String prefix) { return str.regionMatches(true, 0, prefix, 0, prefix.length()); } public long getFileSize(String fileName) throws IOException, InterruptedException { ProcessBuilder builder = getBuilder(); builder.command().add("-I"); builder.command().add(uri + fileName); long length = 0; try (ProcessRunner processor = new ProcessRunner(builder.start(), false)) { StringBuilder errText = new StringBuilder(); processor.captureAllTextInBackground(processor.getStdErr(), errText); for (String line : processor.enumLines(processor.getStdOut())) { if (line.startsWith(BackupConstants.CONTENT_LENGTH_HEADER)) { String lenStr = line.substring(BackupConstants.CONTENT_LENGTH_HEADER.length() + 1); length = Long.parseLong(lenStr); } } int exitCode = processor.join(); if (exitCode != 0 && exitCode != BackupConstants.FILE_DOES_NOT_EXIST) { throw new IOException(errText.length() > 0 ? errText.toString() : Integer.toString(exitCode)); } } return length; } public OutputStream upload(String fileName, long offset) throws Exception { ProcessBuilder builder = getBuilder(); // We should send a "REST offset" command, but the earliest stage we can --quote it is before PASV/EPSV // (then "TYPE I", "STOR ..."), which does not comply to RFC959 that saying REST should be sent // just before STOR. // Here we assume the file on server is not changed after caller determined the offset - which should be // the size of the file on server, so we can just do an append. // We'll not do additional check to see if the file on server is really <offset> long right now, because // even so there is still a chance someone just appended to that file after our checking, it makes no // difference. if (offset > 0) { builder.command().add("-a"); } builder.command().add("-T"); builder.command().add("-"); builder.command().add(uri + fileName); return new ProcessOutputStream(builder.start()); } public List<String> listFiles(String prefix) throws Exception { ProcessBuilder builder = getBuilder(); builder.command().add("-l"); builder.command().add(uri); List<String> fileList = new ArrayList<String>(); try (ProcessRunner processor = new ProcessRunner(builder.start(), false)) { StringBuilder errText = new StringBuilder(); processor.captureAllTextInBackground(processor.getStdErr(), errText); for (String line : processor.enumLines(processor.getStdOut())) { log.info("File name: {}", line); if (!line.endsWith(BackupConstants.COMPRESS_SUFFIX)) { continue; } if (prefix == null || line.startsWith(prefix)) { fileList.add(line); log.info("Listing {}", line); } } int exitCode = processor.join(); if (exitCode != 0) { log.error("List files on FTP {} failed, Exit code {}", uri, exitCode); throw new IOException(errText.length() > 0 ? errText.toString() : Integer.toString(exitCode)); } } return fileList; } public List<String> listAllFiles() throws Exception { return listFiles(null); } public void rename(String sourceFileName, String destFileName) throws Exception { if (uri == null) { throw new IllegalStateException("uri is null"); } URI serverUri = new URI(uri); String endpoint = serverUri.getScheme() + "://" + serverUri.getAuthority(); String path = serverUri.getPath(); String sourceName = (new File(path, sourceFileName)).toString().substring(1); String destName = (new File(path, destFileName)).toString().substring(1); ProcessBuilder builder = getBuilder(); builder.command().add(endpoint); builder.command().add("-Q"); builder.command().add("RNFR " + sourceName); builder.command().add("-Q"); builder.command().add("RNTO " + destName); log.info("cmd={}", hidePassword(builder.command())); try (ProcessRunner processor = new ProcessRunner(builder.start(), false)) { StringBuilder errText = new StringBuilder(); processor.captureAllTextInBackground(processor.getStdErr(), errText); int exitCode = processor.join(); if (exitCode != 0) { log.error("Rename files on FTP {} failed, Exit code {}", uri, exitCode); throw new IOException(errText.length() > 0 ? errText.toString() : Integer.toString(exitCode)); } } } public InputStream download(String backupFileName) throws IOException { ProcessBuilder builder = getBuilder(); String remoteBackupFile = uri + backupFileName; builder.command().add(remoteBackupFile); return new ProcessInputStream(builder.start()); } public String getUri() { return this.uri; } public void validate() throws AuthenticationException, ConnectException { ProcessBuilder builder = getBuilder(); builder.command().add("-I"); builder.command().add("--connect-timeout"); builder.command().add("30"); if (uri.endsWith("/")) { builder.command().add(uri); }else { builder.command().add(uri + "/"); } int exitCode; StringBuilder errText; try { ProcessRunner processor = new ProcessRunner(builder.start(), false); errText = new StringBuilder(); processor.captureAllTextInBackground(processor.getStdErr(), errText); exitCode = processor.join(); } catch (Exception e) { throw new ConnectException(e.getMessage()); } if (exitCode != 0) { if (exitCode == 67) { throw new AuthenticationException(errText.length() > 0 ? errText.toString() : Integer.toString(exitCode)); } else { throw new ConnectException(errText.length() > 0 ? errText.toString() : Integer.toString(exitCode)); } } } // just show the first letter of password private String hidePassword(List<String> command) { String credential = command.get(3); return command.toString().replace(credential, credential.substring(0, (credential.indexOf(":") + 1)) + "***"); } }