com.vmware.aurora.vc.VcFileManager.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.aurora.vc.VcFileManager.java

Source

/***************************************************************************
 * Copyright (c) 2012-2015 VMware, Inc. All Rights Reserved.
 * 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.vmware.aurora.vc;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.log4j.Logger;

import com.vmware.aurora.exception.BaseVMException;
import com.vmware.aurora.util.AuAssert;
import com.vmware.aurora.vc.VcTask.TaskType;
import com.vmware.aurora.vc.VcTaskMgr.IVcTaskBody;
import com.vmware.aurora.vc.vcservice.VcContext;
import com.vmware.aurora.vc.vcservice.VcService;
import com.vmware.vim.binding.impl.vim.host.DatastoreBrowser_Impl;
import com.vmware.vim.binding.vim.Datastore;
import com.vmware.vim.binding.vim.FileManager;
import com.vmware.vim.binding.vim.HttpNfcLease;
import com.vmware.vim.binding.vim.VirtualDiskManager;
import com.vmware.vim.binding.vim.VirtualDiskManager.VirtualDiskSpec;
import com.vmware.vim.binding.vim.fault.FileNotFound;
import com.vmware.vim.binding.vim.fault.Timedout;
import com.vmware.vim.binding.vim.host.DatastoreBrowser;
import com.vmware.vim.binding.vim.host.DatastoreBrowser.FileInfo;
import com.vmware.vim.binding.vim.host.DatastoreBrowser.SearchResults;
import com.vmware.vim.binding.vim.host.DatastoreBrowser.SearchSpec;

/**
 * This is a collection of utility functions to do
 * file operations in VC datastores.
 */
public class VcFileManager {
    private static final Logger logger = Logger.getLogger(VcFileManager.class);

    /**
     * Maximum size of ovf description we'll take.
     */
    static final long MAX_OVF_SIZE = 64 * 1024;
    static final String DS_PATH_PATTERN = "^\\[([^\\]]+)\\](.+)$";

    /*
     * load OVF contents from a file.
     */
    static private String loadOvfContents(String ovfPath) throws IOException {
        BufferedReader reader = null;
        char[] ovfBuf = null;
        try {
            File ovfFile = new File(ovfPath);
            reader = new BufferedReader(new FileReader(ovfFile));
            AuAssert.check(ovfFile.length() < MAX_OVF_SIZE);
            int totalLen = (int) (ovfFile.length() < MAX_OVF_SIZE ? ovfFile.length() : MAX_OVF_SIZE);
            ovfBuf = new char[totalLen];
            int len, offset = 0;
            while (offset < totalLen && (len = reader.read(ovfBuf, offset, totalLen - offset)) != -1) {
                offset += len;
            }
        } finally {
            if (reader != null) {
                reader.close();
            }
        }

        return new String(ovfBuf);
    }

    /**
     * {@link ProgressListener}  keeps track of percentage progress of a job composed
     * of multiple tasks and update the {@link HttpNfcLease} periodically.
     * The total percentage of all tasks is no more than 100.
     * When a task is done, the listener's {@link lenDone} of finished task is
     * incremented by the task's workload of the entire job.
     * When a task is in progress, incremental work is added temporarily
     * to {@link lenDone} for reporting.
     */
    private static class ProgressListener {
        private long lenDone = 0;
        private long pctLen; // length for finishing 1 percent
        private final HttpNfcLease nfcLease;
        private int updatePeriod;
        private long lastTime;
        private volatile int lastProgress = 0;

        public ProgressListener(HttpNfcLease nfcLease, long totalLen) {
            this.nfcLease = nfcLease;
            if (totalLen <= 0) {
                this.pctLen = Long.MAX_VALUE;
            } else {
                this.pctLen = totalLen / 100 + 1;
            }
            if (nfcLease != null) {
                updatePeriod = nfcLease.getInfo().getLeaseTimeout() * 1000 / 2;
                if (updatePeriod > 5000) {
                    updatePeriod = 5000; // update progress at least every 5 seconds
                }
            } else {
                updatePeriod = 5000;
            }
            lastTime = System.currentTimeMillis();
        }

        private void updateProgress(int progress) {
            if (progress >= lastProgress) {
                lastProgress = progress;
                if (nfcLease != null) {
                    nfcLease.progress(lastProgress, null);
                }
            }
        }

        /**
         * Update intermediate progress for the current task.
         * @throws Timedout
         */
        public void updateTask(long len) throws Timedout {
            if (System.currentTimeMillis() - lastTime > updatePeriod) {
                updateProgress((int) ((lenDone + len) / pctLen));
            }
        }

        /**
         * Done with the current task and update progress.
         */
        public void taskDone(long taskLen) throws Timedout {
            lenDone += taskLen;
            updateProgress((int) (lenDone / pctLen));
        }

        public long getLenDone() {
            return lenDone;
        }

        public void setLenDone(long lenDone) throws Timedout {
            this.lenDone = lenDone;
        }
    }

    /*
     * A RequestEntity implementation for file with calls to progress listener.
     */
    private static class ProgressListenerRequestEntity implements RequestEntity {
        private File file;
        private String contentType;
        private ProgressListener listener;
        // Change from 16k to 512k to speed up deploy process
        private final int BUF_SIZE = 1024 * 512;

        public ProgressListenerRequestEntity(File file, String contentType, ProgressListener listener) {
            this.file = file;
            this.contentType = contentType;
            this.listener = listener;
        }

        @Override
        public long getContentLength() {
            return file.length();
        }

        @Override
        public String getContentType() {
            return contentType;
        }

        @Override
        public boolean isRepeatable() {
            return true;
        }

        @Override
        public void writeRequest(final OutputStream out) throws IOException {
            byte[] buf = new byte[BUF_SIZE];
            int i = 0;
            long offset = 0;
            InputStream instream = new FileInputStream(file);
            try {
                while ((i = instream.read(buf)) >= 0) {
                    out.write(buf, 0, i);
                    offset += i;
                    listener.updateTask(offset);
                }
                listener.taskDone(offset);
            } catch (Timedout e) {
                throw new IOException("Progress listener failed.", e);
            } finally {
                instream.close();
            }
        }
    }

    /*
     * Upload file to a given URL.
     */
    static private void uploadFileWork(String url, boolean isPost, File file, String contentType, String cookie,
            ProgressListener listener) throws Exception {
        EntityEnclosingMethod method;
        final RequestEntity entity = new ProgressListenerRequestEntity(file, contentType, listener);
        if (isPost) {
            method = new PostMethod(url);
            method.setContentChunked(true);
        } else {
            method = new PutMethod(url);
            method.addRequestHeader("Cookie", cookie);
            method.setContentChunked(false);
            HttpMethodParams params = new HttpMethodParams();
            params.setBooleanParameter(HttpMethodParams.USE_EXPECT_CONTINUE, true);
            method.setParams(params);
        }
        method.setRequestEntity(entity);

        logger.info("upload " + file + " to " + url);
        long t1 = System.currentTimeMillis();
        boolean ok = false;
        try {
            HttpClient httpClient = new HttpClient();
            int statusCode = httpClient.executeMethod(method);
            String response = method.getResponseBodyAsString(100);
            logger.debug("status: " + statusCode + " response: " + response);
            if (statusCode != HttpStatus.SC_CREATED && statusCode != HttpStatus.SC_OK) {
                throw new Exception("Http post failed");
            }
            method.releaseConnection();
            ok = true;
        } finally {
            if (!ok) {
                method.abort();
            }
        }
        long t2 = System.currentTimeMillis();
        logger.info("upload " + file + " done in " + (t2 - t1) + " ms");
    }

    /*
     * Get VC File URL.
     */
    private static String getVcFileUrl(VcDatastore datastore, String dsFilePath) throws Exception {
        VcService vcs = VcContext.getService();
        String rootFolderUrl = vcs.getServiceUrl().replaceFirst("sdk", "folder");
        AuAssert.check(rootFolderUrl.endsWith("folder"));
        //construct upload file destination URL
        String relativePath = URLEncoder.encode(String.format("%s?dcPath=%s&dsName=%s", dsFilePath,
                datastore.getDatacenter().getURLName(), datastore.getURLName()), "UTF-8");
        return String.format("%s/%s", rootFolderUrl, relativePath);
    }

    static private void uploadFileLoop(String hostUrl, File file, VcDatastore ds, String dsFilePath,
            ProgressListener listener) throws Exception {
        Exception e1 = null;
        String vcUrl = getVcFileUrl(ds, dsFilePath);
        // Using vmware-streamVmdk is 2x as fast as octet-stream
        final String hostContentType = "application/x-vnd.vmware-streamVmdk";
        final String vcContentType = "application/octet-stream";
        // Retry 3 times using host URL
        // XXX VC folder uploading is not completely implemented.
        Boolean reqsUseVc[] = new Boolean[] { false, false, false };
        long lenDone = listener.getLenDone();
        VcService vcs = VcContext.getService();
        for (Boolean useVc : reqsUseVc) {
            try {
                if (useVc) {
                    /*
                     * If we fail to upload to host directly, try to use
                     * "VC folder put API", which is orders of magnitude slower.
                     */
                    String sessionString = "vmware_soap_session=" + vcs.getClientSessionId();
                    uploadFileWork(vcUrl, false, file, vcContentType, sessionString, listener);
                } else {
                    uploadFileWork(hostUrl, true, file, hostContentType, null, listener);
                }
                return;
            } catch (Exception e) {
                e1 = e;
                logger.info("failed to upload file " + file + " to " + hostUrl + ":" + e.getMessage());
                listener.setLenDone(lenDone);
            }
        }
        if (e1 != null) {
            throw e1;
        }
    }

    /**
     * Import an OVF as a virtual machine to a datastore.
     * @param name name of the VM
     * @param rp a resource pool connected to the datastore
     * @param ds the destination datastore
     * @param network default network setting
     * @param ipPolicy default ip allocation policy
     * @param ovfPath OVF file path to be imported.
     * @return the imported VM
     * @throws Exception
     */
    //   static public VcVirtualMachine
    //   importVm(String name, VcResourcePool rp, VcDatastore ds,
    //            VcNetwork network, String ovfPath)
    //   throws Exception {
    //      ManagedObjectReference vmRef;
    //      AuAssert.check(VcContext.isInTaskSession());
    //      VcService vcs = VcContext.getService();
    //
    //      CreateImportSpecParams importParams = new CreateImportSpecParamsImpl();
    //      importParams.setDeploymentOption("");
    //      importParams.setLocale("");
    //      importParams.setEntityName(name);
    //      NetworkMapping[] nets = {
    //            new NetworkMappingImpl("Network 1", network.getMoRef()),
    //            new NetworkMappingImpl("Network 2", network.getMoRef())
    //      };
    //      importParams.setNetworkMapping(nets);
    //      importParams.setIpAllocationPolicy(IpAllocationPolicy.transientPolicy.toString());
    //      importParams.setDiskProvisioning("thin");
    //
    //      // create import spec from ovf
    //      CreateImportSpecResult specResult = vcs.getOvfManager().createImportSpec(
    //            loadOvfContents(ovfPath), rp.getMoRef(), ds.getMoRef(), importParams);
    //      AuAssert.check(specResult.getError() == null && specResult.getWarning() == null);
    //      VmImportSpec importSpec = (VmImportSpec)specResult.getImportSpec();
    //      // start importing the vApp and get the lease to upload vmdks
    //      HttpNfcLease nfcLease = rp.importVApp(importSpec);
    //
    //      // total bytes to be imported
    //      long importTotal = 0;
    //      // map: deviceId -> File
    //      HashMap<String, File> fileMap = new HashMap<String, File>();
    //      String basePath = new File(ovfPath).getParent();
    //      for (FileItem item : specResult.getFileItem()) {
    //         File f = new File(basePath + File.separator + item.getPath());
    //         fileMap.put(item.getDeviceId(), f);
    //         importTotal += f.length();
    //      }
    //
    //      try {
    //         // wait for nfc lease to become ready
    //         State state = nfcLease.getState();
    //         while (state != State.ready) {
    //            Thread.sleep(1000);
    //            state = nfcLease.getState();
    //            if (state == State.error) {
    //               Exception e = nfcLease.getError();
    //               logger.error(e.getMessage(), e.getCause());
    //               throw e;
    //            }
    //         }
    //
    //        nfcLease.progress(0);
    //        ProgressListener listener = new ProgressListener(nfcLease, importTotal);
    //        vmRef = nfcLease.getInfo().getEntity();
    //         //@TODO 1. if this method will be used in future, the TrustManager need to be refactored.
    //        //ThumbprintTrustManager tm = HttpsConnectionUtil.getThumbprintTrustManager();
    //         // upload all files
    //         for(DeviceUrl deviceUrl : nfcLease.getInfo().getDeviceUrl()) {
    //            File f = fileMap.get(deviceUrl.getImportKey());
    //            String thumbprint = deviceUrl.getSslThumbprint();
    //            //@TODO 2. if this method will be used in future, the TrustManager need to be refactored.
    //            //tm.add(thumbprint.toString(), Thread.currentThread());
    //            try {
    //               uploadFileLoop(deviceUrl.getUrl(), f, ds,
    //                              name + "/" + f.getName(), listener);
    //            } finally {
    //               //@TODO 3. if this method will be used in future, the TrustManager need to be refactored.
    //               //tm.remove(thumbprint.toString(), Thread.currentThread());
    //            }
    //         }
    //         nfcLease.progress(100);
    //         nfcLease.complete();
    //      } catch (Exception e) {
    //         logger.error(e.getCause());
    //         try {
    //            /*
    //             * By aborting the lease, VC also deletes the VM.
    //             */
    //            nfcLease.abort(null);
    //         } catch (Exception e1) {
    //            logger.error("got exception trying to abort nfcLease", e1);
    //         }
    //         throw VcException.UPLOAD_ERROR(e);
    //      }
    //
    //      return VcCache.get(vmRef);
    //   }

    public static void uploadFile(String localPath, VcDatastore datastore, String datastorePath) throws Exception {
        AuAssert.check(VcContext.isInTaskSession());
        VcService vcs = VcContext.getService();

        final String url = VcFileManager.getVcFileUrl(datastore, datastorePath);
        final String vcContentType = "application/octet-stream";
        final String sessionString = "vmware_soap_session=" + vcs.getClientSessionId();
        final File file = new File(localPath);
        final ProgressListener progress = new ProgressListener(null, 0);

        uploadFileWork(url, false, file, vcContentType, sessionString, progress);
    }

    public static String getDsFromPath(String dsPath) throws Exception {
        Pattern pattern = Pattern.compile(DS_PATH_PATTERN);
        Matcher match = pattern.matcher(dsPath);
        AuAssert.check(match.matches());
        return match.group(1);
    }

    /**
     * Get the datastore path.
     * @param ds datastore
     * @param path pathname to the file inside a datastore
     * @return full datastore pathname
     */
    public static String getDsPath(VcDatastore ds, String path) {
        // shouldn't be formatted already
        AuAssert.check(!path.matches(DS_PATH_PATTERN));
        return String.format("[%s] %s", ds.getURLName(), path);
    }

    /**
     * Get the datastore path for a file under the VM directory.
     * @param vm virtual machine object
     * @param name file name
     * @return
     */
    public static String getDsPath(VcVirtualMachine vm, String name) {
        return String.format("%s/%s", vm.getPathName(), name);
    }

    /**
     * Get the datastore path for a file under the VM directory on a different datastore.
     * The file would be either at the root of the datastore name-prefixed with the VM
     * name or in a directory with same name as the VM directory.
     * @param vm virtual machine object
     * @param ds datastore (null means the default VM datastore)
     * @param name file name
     * @return
     */
    public static String getDsPath(VcVirtualMachine vm, VcDatastore ds, String name) {
        if (ds == null) {
            return getDsPath(vm, name);
        } else {
            try {
                return String.format("[%s]", ds.getName());
            } catch (Exception ex) {
                throw BaseVMException.INVALID_FILE_PATH(ex, vm.getPathName());
            }
        }
    }

    /**
     * Extract disk name of a given disk file name
     * @param diskFileName the given disk file name
     */
    public static String getDiskName(String diskFileName) {
        return diskFileName.substring(diskFileName.lastIndexOf('/') + 1);
    }

    /**
     * Copy virtual disk from source datastore path to destination datastore
     * path.
     *
     * @param srcDsPath
     *           source pathname to the file
     * @param srcDc
     *           source datacenter
     * @param dstDsPath
     *           destination pathname to the file
     * @param dstDc
     *           destination datacenter
     * @param diskSpec
     *           destination virtual disk specification
     * @throws Exception
     */
    protected static VcTask copyVirtualDisk(final String srcDsPath, final VcDatacenter srcDc,
            final String dstDsPath, final VcDatacenter dstDc, final VirtualDiskSpec diskSpec,
            final IVcTaskCallback callback) throws Exception {
        VcTask task = VcContext.getTaskMgr().execute(new IVcTaskBody() {
            @Override
            public VcTask body() throws Exception {
                final VirtualDiskManager mgr = VcContext.getService().getVirtualDiskManager();
                return new VcTask(TaskType.CopyVmdk, mgr.copyVirtualDisk(srcDsPath, srcDc.getMoRef(), dstDsPath,
                        dstDc.getMoRef(), diskSpec, true), callback);
            }
        });
        return task;
    }

    protected static void copyVirtualDisk(final String srcDsPath, final VcDatacenter srcDc, final String dstDsPath,
            final VcDatacenter dstDc, final VirtualDiskSpec diskSpec) throws Exception {
        VcTask task = copyVirtualDisk(srcDsPath, srcDc, dstDsPath, dstDc, diskSpec, null);
        task.waitForCompletion();
    }

    /**
     * Copy virtual disk from source datastore path to destination datastore
     * path.
     *
     * @param srcDsPath
     *           source pathname to the file
     * @param srcDc
     *           source datacenter
     * @param dstDsPath
     *           destination pathname to the file
     * @param dstDc
     *           destination datacenter
     * @throws Exception
     */
    protected static VcTask moveVirtualDisk(final String srcDsPath, final VcDatacenter srcDc,
            final String dstDsPath, final VcDatacenter dstDc, final IVcTaskCallback callback) throws Exception {
        VcTask task = VcContext.getTaskMgr().execute(new IVcTaskBody() {
            @Override
            public VcTask body() throws Exception {
                final VirtualDiskManager mgr = VcContext.getService().getVirtualDiskManager();
                return new VcTask(TaskType.CopyVmdk,
                        mgr.moveVirtualDisk(srcDsPath, srcDc.getMoRef(), dstDsPath, dstDc.getMoRef(), true, null),
                        callback);
            }
        });
        return task;
    }

    public static void moveVirtualDisk(final String srcDsPath, final VcDatacenter srcDc, final String dstDsPath,
            final VcDatacenter dstDc) throws Exception {
        VcTask task = moveVirtualDisk(srcDsPath, srcDc, dstDsPath, dstDc, null);
        task.waitForCompletion();
    }

    /**
     * Delete the virtual disk using its data store path.
     *
     * @param dsPath
     *           pathname to the data-store.
     * @param dc
     *           datacenter
     * @throws Exception
     */
    protected static VcTask deleteVirtualDisk(final String dsPath, final VcDatacenter dc,
            final IVcTaskCallback callback) throws Exception {
        VcTask task = VcContext.getTaskMgr().execute(new IVcTaskBody() {
            @Override
            public VcTask body() throws Exception {
                VirtualDiskManager mgr = VcContext.getService().getVirtualDiskManager();
                return new VcTask(TaskType.DeleteVmdk, mgr.deleteVirtualDisk(dsPath, dc.getMoRef()), callback);
            }
        });
        return task;
    }

    public static void deleteVirtualDisk(final String dsPath, final VcDatacenter dc) throws Exception {
        VcTask task = deleteVirtualDisk(dsPath, dc, null);
        task.waitForCompletion();
    }

    /**
     * Get the UUID of a virtual disk using its datastore path.
     * @param dsPath pathname in the datastore
     * @param dc datacenter
     * @return UUID of the disk
     * @throws Exception
     */
    public static String queryVirtualDiskUuid(final String dsPath, final VcDatacenter dc) throws Exception {
        final VirtualDiskManager mgr = VcContext.getService().getVirtualDiskManager();
        return mgr.queryVirtualDiskUuid(dsPath, dc.getMoRef());
    }

    /**
     * Copy file from source datastore path to destination datastore path.
     * @param srcDs source datastore
     * @param srcPath source pathname to the file
     * @param dstDs destination datastore
     * @param dstPath destination pathname to the file
     * @throws Exception
     */
    public static VcTask copyFile(final VcDatastore srcDs, final String srcPath, final VcDatastore dstDs,
            final String dstPath, final IVcTaskCallback callback) throws Exception {
        final String srcDsPath = getDsPath(srcDs, srcPath);
        final String dstDsPath = getDsPath(dstDs, dstPath);
        VcTask task = VcContext.getTaskMgr().execute(new IVcTaskBody() {
            @Override
            public VcTask body() throws Exception {
                FileManager mgr = VcContext.getService().getFileManager();
                return new VcTask(TaskType.CopyFile, mgr.copyFile(srcDsPath, srcDs.getDatacenterMoRef(), dstDsPath,
                        dstDs.getDatacenterMoRef(), true), callback);
            }
        });
        return task;
    }

    public static void copyFile(final VcDatastore srcDs, final String srcPath, final VcDatastore dstDs,
            final String dstPath) throws Exception {
        VcTask task = copyFile(srcDs, srcPath, dstDs, dstPath, null);
        task.waitForCompletion();
    }

    /**
     * Delete a file or directory in a datastore.
     * @param datastore
     * @param filePath
     * @throws Exception
     */
    public static VcTask deleteFile(final VcDatastore datastore, final String filePath,
            final IVcTaskCallback callback) throws Exception {
        final String dsPath = getDsPath(datastore, filePath);
        VcTask task = VcContext.getTaskMgr().execute(new IVcTaskBody() {
            @Override
            public VcTask body() throws Exception {
                FileManager mgr = VcContext.getService().getFileManager();
                return new VcTask(TaskType.DeleteFile, mgr.deleteFile(dsPath, datastore.getDatacenterMoRef()),
                        callback);
            }
        });
        return task;
    }

    public static void deleteFile(final VcDatastore datastore, final String filePath) throws Exception {
        VcTask task = deleteFile(datastore, filePath, null);
        task.waitForCompletion();
    }

    /**
     * Move file from source datastore path to destination datastore path.
     * @param srcDs source datastore
     * @param srcPath source pathname to the file
     * @param dstDs destination datastore
     * @param dstPath destination pathname to the file
     * @throws Exception
     */
    public static VcTask moveFile(final VcDatastore srcDs, final String srcPath, final VcDatastore dstDs,
            final String dstPath, final IVcTaskCallback callback) throws Exception {
        final String srcDsPath = getDsPath(srcDs, srcPath);
        final String dstDsPath = getDsPath(dstDs, dstPath);
        VcTask task = VcContext.getTaskMgr().execute(new IVcTaskBody() {
            @Override
            public VcTask body() throws Exception {
                FileManager mgr = VcContext.getService().getFileManager();
                return new VcTask(TaskType.MoveFile, mgr.moveFile(srcDsPath, srcDs.getDatacenterMoRef(), dstDsPath,
                        dstDs.getDatacenterMoRef(), true), callback);
            }
        });
        return task;
    }

    public static void moveFile(final VcDatastore srcDs, final String srcPath, final VcDatastore dstDs,
            final String dstPath) throws Exception {
        VcTask task = moveFile(srcDs, srcPath, dstDs, dstPath, null);
        task.waitForCompletion();
    }

    private static VcTask searchFile(final VcDatastore ds, final String filePath, final IVcTaskCallback callback)
            throws Exception {
        // split file path into dir and fname
        int index = filePath.lastIndexOf('/');
        String fname = filePath.substring(index + 1, filePath.length());
        String fileDir = filePath.substring(0, index);

        final String dsPath = getDsPath(ds, fileDir);
        final SearchSpec spec = new DatastoreBrowser_Impl.SearchSpecImpl();
        spec.setMatchPattern(new String[] { fname });
        VcTask task = VcContext.getTaskMgr().execute(new IVcTaskBody() {
            @Override
            public VcTask body() throws Exception {
                Datastore mo = ds.getManagedObject();
                DatastoreBrowser browser = MoUtil.getManagedObject(mo.getBrowser());
                return new VcTask(TaskType.SearchFile, browser.search(dsPath, spec), callback);
            }
        });
        return task;
    }

    /**
     * Search for a file in a datastore.
     * @param ds datastore
     * @param filePath file path in the datastore
     * @return file name if the file exists
     * @throws Exception
     */
    public static String searchFile(final VcDatastore ds, final String filePath) throws Exception {
        try {
            VcTask task = searchFile(ds, filePath, null);
            // XXX This task frequently raises a WARN in VcTask for dropped VC taskInfo.state.
            task.waitForCompletion();
            SearchResults results = (SearchResults) task.getTaskResult();
            FileInfo[] files = results.getFile();
            if (files != null && files.length > 0) {
                AuAssert.check(files.length == 1);
                // Only return the file name. It's directory is reported elsewhere.
                return files[0].getPath();
            }
            return null;
        } catch (FileNotFound e) {
            // return null if the file is not found
            return null;
        }
    }
}