/*
* $Id$
*
* Copyright (C) 2003-2014 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library 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 Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.command.file;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import org.jnode.shell.AbstractCommand;
import org.jnode.shell.syntax.Argument;
import org.jnode.shell.syntax.FileArgument;
import org.jnode.shell.syntax.FlagArgument;
/**
* File copy utility. This utility copies one file to another file, or multiple files or directories
* into an existing directory. Files are copied byte-wise (not character-wise). Recursive directory
* copy is supported.
*
* @author crawley@jnode.org
*/
public class CpCommand extends AbstractCommand {
private static final String HELP_SOURCE = "source files or directories";
private static final String HELP_TARGET = "target file or directory";
private static final String HELP_FORCE = "if set, force overwrite of existing files";
private static final String HELP_INTERACTIVE = "if set, ask before overwriting existing files";
private static final String HELP_UPDATE = "if set, overwrite existing files older than their source";
private static final String HELP_RECURSE = "if set, recursively copy source directories";
private static final String HELP_VERBOSE = "if set, output a line for each file copied";
private static final String HELP_SUPER = "Copy files and directories";
private static final String ERR_NO_SOURCE = "No source files or directories supplied";
private static final String ERR_NO_WRITE = "Target directory is not writable";
private static final String ERR_MULTI_DIR = "Multi-file copy requires the target to be a directory";
private static final String ERR_COPY_DIR_FILE = "Cannot copy a directory to a file";
private static final String ERR_COPY_DEV = "Cannot copy to a device";
private static final String FMT_VERBOSE_COPY = "File copied: %d, directories created: %d%n";
private static final String ERR_MUTEX_FLAGS = "The force, interactive and update flags are mutually exclusive";
private static final String FMT_NO_WRITE = "directory '%s' is not writeable";
private static final String FMT_DIR_CREATE = "Creating directory '%s'%n";
private static final String FMT_DIR_REPLACE = "Replacing file '%s' with a directory%n";
private static final String FMT_DIR_SKIP = "not overwriting '%s' with a directory";
private static final String FMT_IS_DIR = "'%s' is a directory";
private static final String FMT_COPY_FILE = "Copying file '%s' as '%s'%n";
private static final String FMT_SRC_NOEXIST = "'%s' does not exist";
private static final String FMT_SRC_NOREAD = "'%s' cannot be read";
private static final String FMT_SRC_DEVICE = "'%s' is a device";
private static final String FMT_COPY_DIR_SELF = "Cannot copy directory '%s' into itself";
private static final String FMT_COPY_FILE_SELF = "Cannot copy file '%s' to itself";
private static final String FMT_COPY_SUB = "Cannot copy directory '%s' into a subdirectory ('%s')";
private static final String FMT_NO_COPY_DIR = "Cannot copy '%s' onto directory '%s'";
private static final String FMT_NO_COPY_DEV = "Cannot copy '%s' to device '%s'";
private static final String FMT_EXISTS = "'%s' already exists";
private static final String FMT_NEWER = "'%s' is newer than '%s'";
private static final String FMT_ASK_OVERWRITE = "Overwrite '%s' with '%s'? [y/n]%n";
private static final String ERR_COPY_EOF = "EOF - abandoning copying";
private static final String ERR_COPY_IOEX = "IO Error - abandoning copying";
private static final String STR_ASK_AGAIN = "Answer 'y' or 'n'";
private static final String FMT_SKIP = "%s: skipping%n";
static final byte MODE_NORMAL = 0;
static final byte MODE_INTERACTIVE = 1;
static final byte MODE_FORCE = 2;
static final byte MODE_UPDATE = 3;
private final FileArgument argSource;
private final FileArgument argTarget;
private final FlagArgument argForce;
private final FlagArgument argInteractive;
private final FlagArgument argUpdate;
private final FlagArgument argRecursive;
private final FlagArgument argVerbose;
private byte mode = MODE_NORMAL;
private boolean recursive = false;
private boolean verbose = false;
private int filesCopied = 0;
private int directoriesCreated = 0;
private BufferedReader in;
private PrintWriter out;
private PrintWriter err;
private byte[] buffer = new byte[1024 * 8];
public CpCommand() {
super(HELP_SUPER);
argSource = new FileArgument("source", Argument.MANDATORY | Argument.MULTIPLE | Argument.EXISTING, HELP_SOURCE);
argTarget = new FileArgument("target", Argument.MANDATORY, HELP_TARGET);
argForce = new FlagArgument("force", Argument.OPTIONAL, HELP_FORCE);
argInteractive = new FlagArgument("interactive", Argument.OPTIONAL, HELP_INTERACTIVE);
argUpdate = new FlagArgument("update", Argument.OPTIONAL, HELP_UPDATE);
argRecursive = new FlagArgument("recursive", Argument.OPTIONAL, HELP_RECURSE);
argVerbose = new FlagArgument("verbose", Argument.OPTIONAL, HELP_VERBOSE);
registerArguments(argSource, argTarget, argForce, argInteractive, argRecursive, argUpdate, argVerbose);
}
public static void main(String[] args) throws Exception {
new CpCommand().execute(args);
}
public void execute() throws Exception {
this.out = getOutput().getPrintWriter();
this.err = getError().getPrintWriter();
processFlags();
if (mode == MODE_INTERACTIVE) {
this.in = new BufferedReader(getInput().getReader());
}
File[] sources = argSource.getValues();
File target = argTarget.getValue();
if (sources.length == 0) {
error(ERR_NO_SOURCE);
}
if (target.isDirectory()) {
if (!target.canWrite()) {
error(ERR_NO_WRITE);
}
for (File source : sources) {
if (checkSafe(source, target)) {
copyIntoDirectory(source, target);
}
}
} else if (sources.length > 1) {
error(ERR_MULTI_DIR);
} else {
File source = sources[0];
if (source.isDirectory()) {
error(ERR_COPY_DIR_FILE);
} else if (target.exists() && !target.isFile()) {
error(ERR_COPY_DEV);
} else {
if (checkSafe(source, target)) {
copyToFile(source, target);
}
}
}
if (verbose) {
out.format(FMT_VERBOSE_COPY, filesCopied, directoriesCreated);
}
}
private void processFlags() {
recursive = argRecursive.isSet();
verbose = argVerbose.isSet();
// The mode flags are mutually exclusive ...
if (argForce.isSet()) {
mode = MODE_FORCE;
}
if (argInteractive.isSet()) {
if (mode != MODE_NORMAL) {
error(ERR_MUTEX_FLAGS);
}
mode = MODE_INTERACTIVE;
}
if (argUpdate.isSet()) {
if (mode != MODE_NORMAL) {
error(ERR_MUTEX_FLAGS);
}
mode = MODE_UPDATE;
}
}
/**
* Copy a file or directory into a supplied target directory.
*
* @param source the name of the object to be copied
* @param targetDir the destination directory
* @throws IOException
*/
private void copyIntoDirectory(File source, File targetDir) throws IOException {
if (!targetDir.canWrite()) {
skip(String.format(FMT_NO_WRITE, targetDir));
} else if (source.isDirectory()) {
if (recursive) {
File newDir = new File(targetDir, source.getName());
if (!newDir.exists()) {
if (verbose) {
out.format(FMT_DIR_CREATE, newDir);
}
newDir.mkdir();
directoriesCreated++;
} else if (!newDir.isDirectory()) {
if (mode == MODE_FORCE) {
if (verbose) {
out.format(FMT_DIR_REPLACE, newDir);
}
newDir.delete();
newDir.mkdir();
directoriesCreated++;
} else {
skip(String.format(FMT_DIR_SKIP, newDir));
return;
}
}
String[] contents = source.list();
for (String name : contents) {
if (name.equals(".") || name.equals("..")) {
continue;
}
copyIntoDirectory(new File(source, name), newDir);
}
} else {
skip(String.format(FMT_IS_DIR, source));
}
} else {
File newFile = new File(targetDir, source.getName());
copyToFile(source, newFile);
}
}
/**
* Copy a file to (as) a file
*
* @param sourceFile
* @param targetFile
* @throws IOException
*/
private void copyToFile(File sourceFile, File targetFile) throws IOException {
if (!checkSafe(sourceFile, targetFile) ||
!checkSource(sourceFile) ||
!checkTarget(targetFile, sourceFile)) {
return;
}
if (verbose) {
out.format(FMT_COPY_FILE, sourceFile, targetFile);
}
InputStream sin = null;
OutputStream tout = null;
try {
sin = new FileInputStream(sourceFile);
tout = new FileOutputStream(targetFile);
while (true) {
int nosBytesRead = sin.read(buffer);
if (nosBytesRead <= 0) {
break;
}
tout.write(buffer, 0, nosBytesRead);
}
} finally {
if (sin != null) {
try {
sin.close();
} catch (IOException ex) {
// ignore
}
}
if (tout != null) {
try {
tout.close();
} catch (IOException ex) {
// ignore
}
}
}
filesCopied++;
}
/**
* Check that a source object exists, is readable and is either
* a file or a directory.
*
* @param source
* @return
*/
private boolean checkSource(File source) {
if (!source.exists()) {
return skip(String.format(FMT_SRC_NOEXIST, source));
} else if (!source.canRead()) {
return skip(String.format(FMT_SRC_NOREAD, source));
} else if (!(source.isFile() || source.isDirectory())) {
return vskip(String.format(FMT_SRC_DEVICE, source));
} else {
return true;
}
}
/**
* Check that a copy is going to be safe. Unsafe things are copying a
* file to itself and copying a directory into itself or a subdirectory.
*
* @param source
* @param target
* @return
* @throws IOException
*/
private boolean checkSafe(File source, File target) throws IOException {
// These checks must be done with canonical paths. But fortunately they
// don't need to be repeated for every file/directory in a recursive copy.
String sourcePath = source.getCanonicalPath();
String targetPath = target.getCanonicalPath();
if (target.isDirectory()) {
if (recursive && source.isDirectory()) {
if (sourcePath.equals(targetPath)) {
return skip(String.format(FMT_COPY_DIR_SELF, source));
}
if (!sourcePath.endsWith(File.separator)) {
sourcePath = sourcePath + File.separatorChar;
}
if (targetPath.startsWith(sourcePath)) {
return skip(String.format(FMT_COPY_SUB, source, target));
}
}
} else {
if (sourcePath.equals(targetPath)) {
return skip(String.format(FMT_COPY_FILE_SELF, source));
}
}
return true;
}
/**
* Check that the target can be written / overwritten. In interactive mode,
* the user is asked about clobbering existing files. In update mode, they
* are overwritten if the source is newer than the target. In force mode, they
* are clobbered without asking. In normal mode, existing target files are
* skipped.
*
* @param target
* @param source
* @return
*/
private boolean checkTarget(File target, File source) {
if (!target.exists()) {
return true;
}
if (target.isDirectory() && !source.isDirectory()) {
return skip(String.format(FMT_NO_COPY_DIR, source, target));
}
if (!target.isFile()) {
return vskip(String.format(FMT_NO_COPY_DEV, source, target));
}
switch (mode) {
case MODE_NORMAL:
return vskip(String.format(FMT_EXISTS, target));
case MODE_FORCE:
return true;
case MODE_UPDATE:
return (source.lastModified() > target.lastModified() ||
vskip(String.format(FMT_NEWER, target, source)));
case MODE_INTERACTIVE:
out.format(FMT_ASK_OVERWRITE, target, source);
while (true) {
try {
String line = in.readLine();
if (line == null) {
error(ERR_COPY_EOF);
}
if (line.length() > 0) {
if (line.charAt(0) == 'y' || line.charAt(0) == 'Y') {
return true;
} else if (line.charAt(0) == 'n' || line.charAt(0) == 'N') {
return vskip("'" + target + '\'');
}
}
out.print(STR_ASK_AGAIN);
} catch (IOException ex) {
error(ERR_COPY_IOEX);
}
}
}
return false;
}
private void error(String msg) {
err.println(msg);
exit(1);
}
private boolean skip(String msg) {
err.format(FMT_SKIP, msg);
return false;
}
private boolean vskip(String msg) {
if (verbose) {
err.format(FMT_SKIP, msg);
}
return false;
}
}