/* * Copyright (C) 2014 University of Dundee & Open Microscopy Environment. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package ome.formats.importer.transfers; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.StringWriter; import java.util.UUID; import org.apache.commons.io.FileUtils; import ome.util.checksum.ChecksumProvider; import omero.ServerError; import omero.api.RawFileStorePrx; import omero.model.OriginalFile; /** * Local-only file transfer mechanism which makes use of soft-linking. * This is only useful where the command "ln -s source target" will work. * * @since 5.0 */ public abstract class AbstractExecFileTransfer extends AbstractFileTransfer { private static final String LINE = "\n---------------------------------------------------\n"; private static final String SEPARATOR = System.getProperty("line.separator"); /** * "Transfer" files by soft-linking them into place. This method is likely * re-usable for other general "linking" strategies by overriding * {@link #createProcessBuilder(File, File)} and the other protected methods here. */ public String transfer(TransferState state) throws IOException, ServerError { RawFileStorePrx rawFileStore = start(state); try { final OriginalFile root = state.getRootFile(); final OriginalFile ofile = state.getOriginalFile(); final File location = getLocalLocation(root, ofile); final File file = state.getFile(); final long length = state.getLength(); final ChecksumProvider cp = state.getChecksumProvider(); state.uploadStarted(); checkLocation(location, rawFileStore); // closes rawFileStore state.closeUploader(); exec(file, location); checkTarget(location, state); cp.putFile(file.getAbsolutePath()); state.stop(length); state.uploadBytes(length); return finish(state, length); } finally { state.closeUploader(); } } /** * Build a path of the form "root.path/root.name/file.path/file.name". * * @param root the root directory * @param ofile a path relative to the root * @return the assembled path with separators suitable for the local filesystem */ protected File getLocalLocation(OriginalFile root, OriginalFile ofile) { StringBuilder sb = new StringBuilder(); sb.append(root.getPath().getValue()); sb.append(File.separatorChar); sb.append(root.getName().getValue()); sb.append(File.separatorChar); sb.append(ofile.getPath().getValue()); sb.append(File.separatorChar); sb.append(ofile.getName().getValue()); return new File(sb.toString()); } /** * Check that the target location: 1) doesn't exist and 2) is properly * written to by the server. If either condition fails, no linking takes * place. * * @param location the source file * @param rawFileStore the target on the server * @throws ServerError if the raw file store could not be used * @throws IOException for problems with the source file */ protected void checkLocation(File location, RawFileStorePrx rawFileStore) throws ServerError, IOException { final String uuid = UUID.randomUUID().toString(); // Safety measures if (location.exists()) { throw new RuntimeException(location + " exists!"); } // First we guarantee that we have the right file // If so, we remove it try { rawFileStore.write(uuid.getBytes(), 0, uuid.getBytes().length); } finally { rawFileStore.close(); } try { if (!location.exists()) { throw failLocationCheck(location, "does not exist"); } else if (!location.canRead()) { throw failLocationCheck(location, "cannot be read"); } else if (!uuid.equals(FileUtils.readFileToString(location))) { throw failLocationCheck(location, "does not match check text"); } } finally { if (!location.canWrite()) { throw failLocationCheck(location, "cannot be modified locally"); } else { boolean deleted = FileUtils.deleteQuietly(location); if (!deleted) { throw failLocationCheck(location, "could not be cleaned up"); } } } } protected RuntimeException failLocationCheck(File location, String msg) { StringBuilder sb = new StringBuilder(); sb.append(LINE); sb.append(String.format("Check failed: %s %s!\n", location, msg)); sb.append("You likely do not have access to the ManagedRepository "); sb.append("for in-place import.\n"); sb.append("Aborting..."); sb.append(LINE); throw new RuntimeException(sb.toString()); } /** * Executes a local command and fails on non-0 return codes. * * @param file the source file * @param location the target on the server * @throws IOException for problems with the source file */ protected void exec(File file, File location) throws IOException { ProcessBuilder pb = createProcessBuilder(file, location); pb.redirectErrorStream(true); Process process = pb.start(); Integer rcode = null; while (rcode == null) { try { rcode = process.waitFor(); break; } catch (InterruptedException e) { continue; } } if (rcode == null || rcode.intValue() != 0) { StringWriter sw = new StringWriter(); sw.append("transfer process returned: "); sw.append(Integer.toString(rcode)); sw.append("\n"); sw.append("command:"); for (String arg : pb.command()) { sw.append(" "); sw.append(arg); } sw.append("\n"); sw.append("output:"); sw.append(LINE); String line = ""; BufferedReader br = new BufferedReader( new InputStreamReader(process.getInputStream())); while ( (line = br.readLine()) != null) { sw.append(line); sw.append(SEPARATOR); } sw.append(LINE); String msg = sw.toString(); log.error(msg); throw new RuntimeException(msg); } } /** * Check that the server can properly read the copied file. * * Like {@link #checkLocation(File, RawFileStorePrx)} but <em>after</em> * the invocation of {@link #exec(File, File)}, there is some chance, likely * due to file permissions, that the server will not be able to read the * transfered file. If so, raise an exception and leave the user to cleanup * and modifications. */ protected void checkTarget(File location, TransferState state) throws ServerError { try { state.getUploader("r").size(); } catch (Throwable t) { String message; if (t instanceof ServerError) { message = ((ServerError) t).message; } else { message = t.getMessage(); } StringBuilder sb = new StringBuilder(); sb.append(t.getClass().getName()); sb.append(" : "); sb.append(message); sb.append("\nThe server could not check the size of the file:\n"); sb.append("-----------------------------------------------\n"); sb.append(location); sb.append("\n-----------------------------------------------\n"); sb.append("Most likely the server process has no read access\n"); sb.append("and therefore in-place import cannot proceed. You\n"); sb.append("should delete this file manually if you are sure\n"); sb.append("that the original is safe.\n"); throw new RuntimeException(sb.toString()); } } /** * Creates a {@link ProcessBuilder} instance ready to have * {@link ProcessBuilder#start()} called on it. The only critical * piece of information should be the return code. * * @param file File to be copied. * @param location Location to copy to. * @return an instance ready for performing the transfer */ protected abstract ProcessBuilder createProcessBuilder(File file, File location); protected void printLine() { log.error("*******************************************"); } }