// NAnt - A .NET build tool
// Copyright (C) 2001-2004 Gerry Shaw
//
// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// Gerry Shaw (gerry_shaw@yahoo.com)
// Ian MacLean (imaclean@gmail.com)
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using NAnt.Core.Attributes;
using NAnt.Core.Types;
using NAnt.Core.Util;
using NAnt.Core.Filters;
namespace NAnt.Core.Tasks {
///
/// Copies a file or set of files to a new file or directory.
///
///
///
/// Files are only copied if the source file is newer than the destination
/// file, or if the destination file does not exist. However, you can
/// explicitly overwrite files with the attribute.
///
///
/// When a is used to select files to copy, the
/// attribute must be set. Files that are
/// located under the base directory of the will
/// be copied to a directory under the destination directory matching the
/// path relative to the base directory of the ,
/// unless the attribute is set to
/// .
///
///
/// Files that are not located under the the base directory of the
/// will be copied directly under to the destination
/// directory, regardless of the value of the
/// attribute.
///
/// Encoding
///
/// Unless an encoding is specified, the encoding associated with the
/// system's current ANSI code page is used.
///
///
/// An UTF-8, little-endian Unicode, and big-endian Unicode encoded text
/// file is automatically recognized, if the file starts with the
/// appropriate byte order marks.
///
///
/// If you employ filters in your copy operation, you should limit the copy
/// to text files. Binary files will be corrupted by the copy operation.
///
///
///
///
/// Copy a single file while changing its encoding from "latin1" to
/// "utf-8".
///
///
///
/// ]]>
///
///
///
/// Copy a set of files to a new directory.
///
///
///
///
///
///
/// ]]>
///
///
///
///
/// Copy a set of files to a directory, replacing @TITLE@ with
/// "Foo Bar" in all files.
///
///
///
///
///
///
///
///
///
///
///
///
/// ]]>
///
///
[TaskName("copy")]
public class CopyTask : Task {
#region Private Instance Fields
private FileInfo _sourceFile;
private FileInfo _toFile;
private DirectoryInfo _toDirectory;
private bool _overwrite;
private bool _flatten;
private FileSet _fileset = new FileSet();
private Hashtable _fileCopyMap;
private bool _includeEmptyDirs = true;
private FilterChain _filters;
private Encoding _inputEncoding;
private Encoding _outputEncoding;
#endregion Private Instance Fields
#region Public Instance Constructors
///
/// Initialize new instance of the .
///
public CopyTask() {
if (PlatformHelper.IsUnix) {
_fileCopyMap = new Hashtable();
} else {
_fileCopyMap = CollectionsUtil.CreateCaseInsensitiveHashtable();
}
}
#endregion Public Instance Constructors
#region Public Instance Properties
///
/// The file to copy.
///
[TaskAttribute("file")]
public virtual FileInfo SourceFile {
get { return _sourceFile; }
set { _sourceFile = value; }
}
///
/// The file to copy to.
///
[TaskAttribute("tofile")]
public virtual FileInfo ToFile {
get { return _toFile; }
set { _toFile = value; }
}
///
/// The directory to copy to.
///
[TaskAttribute("todir")]
public virtual DirectoryInfo ToDirectory {
get { return _toDirectory; }
set { _toDirectory = value; }
}
///
/// Overwrite existing files even if the destination files are newer.
/// The default is .
///
[TaskAttribute("overwrite")]
[BooleanValidator()]
public bool Overwrite {
get { return _overwrite; }
set { _overwrite = value; }
}
///
/// Ignore directory structure of source directory, copy all files into
/// a single directory, specified by the
/// attribute. The default is .
///
[TaskAttribute("flatten")]
[BooleanValidator()]
public virtual bool Flatten {
get { return _flatten; }
set { _flatten = value; }
}
///
/// Copy any empty directories included in the .
/// The default is .
///
[TaskAttribute("includeemptydirs")]
[BooleanValidator()]
public bool IncludeEmptyDirs {
get { return _includeEmptyDirs; }
set { _includeEmptyDirs = value; }
}
///
/// Used to select the files to copy. To use a ,
/// the attribute must be set.
///
[BuildElement("fileset")]
public virtual FileSet CopyFileSet {
get { return _fileset; }
set { _fileset = value; }
}
///
/// Chain of filters used to alter the file's content as it is copied.
///
[BuildElement("filterchain")]
public virtual FilterChain Filters {
get { return _filters; }
set { _filters = value; }
}
///
/// The encoding to use when reading files. The default is the system's
/// current ANSI code page.
///
[TaskAttribute("inputencoding")]
public Encoding InputEncoding {
get { return _inputEncoding; }
set { _inputEncoding = value; }
}
///
/// The encoding to use when writing the files. The default is
/// the encoding of the input file.
///
[TaskAttribute("outputencoding")]
public Encoding OutputEncoding {
get { return _outputEncoding; }
set { _outputEncoding = value; }
}
#endregion Public Instance Properties
#region Protected Instance Properties
///
/// The set of files to perform a file operation on.
///
///
///
/// The key of the is the absolute path of
/// the destination file and the value is a
/// holding the path and last write time of the most recently updated
/// source file that is selected to be copied or moved to the
/// destination file.
///
///
/// On Windows, the is case-insensitive.
///
///
protected Hashtable FileCopyMap {
get { return _fileCopyMap; }
}
#endregion Protected Instance Properties
#region Override implementation of Task
///
/// Checks whether the task is initialized with valid attributes.
///
/// The used to initialize the task.
protected override void InitializeTask(XmlNode taskNode) {
if (Flatten && ToDirectory == null) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"'flatten' attribute requires that 'todir' has been set."),
Location);
}
if (ToDirectory == null && CopyFileSet != null && CopyFileSet.Includes.Count > 0) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"The 'todir' should be set when using the element"
+ " to specify the list of files to be copied."), Location);
}
if (SourceFile != null && CopyFileSet != null && CopyFileSet.Includes.Count > 0) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"The 'file' attribute and the element"
+ " cannot be combined."), Location);
}
if (ToFile == null && ToDirectory == null) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"Either the 'tofile' or 'todir' attribute should be set."),
Location);
}
if (ToFile != null && ToDirectory != null) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"The 'tofile' and 'todir' attribute cannot both be set."),
Location);
}
}
///
/// Executes the Copy task.
///
/// A file that has to be copied does not exist or could not be copied.
protected override void ExecuteTask() {
// ensure base directory is set, even if fileset was not initialized
// from XML
if (CopyFileSet.BaseDirectory == null) {
CopyFileSet.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);
}
// Clear previous copied files
_fileCopyMap = new Hashtable();
// copy a single file.
if (SourceFile != null) {
if (SourceFile.Exists) {
FileInfo dstInfo = null;
if (ToFile != null) {
dstInfo = ToFile;
} else {
string dstFilePath = Path.Combine(ToDirectory.FullName,
SourceFile.Name);
dstInfo = new FileInfo(dstFilePath);
}
// do the outdated check
bool outdated = (!dstInfo.Exists) || (SourceFile.LastWriteTime > dstInfo.LastWriteTime);
if (Overwrite || outdated) {
// add to a copy map of absolute verified paths
FileCopyMap.Add(dstInfo.FullName, new FileDateInfo(SourceFile.FullName, SourceFile.LastWriteTime));
if (dstInfo.Exists && dstInfo.Attributes != FileAttributes.Normal) {
File.SetAttributes(dstInfo.FullName, FileAttributes.Normal);
}
}
} else {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"Could not find file '{0}' to copy.", SourceFile.FullName),
Location);
}
} else { // copy file set contents.
// get the complete path of the base directory of the fileset, ie, c:\work\nant\src
DirectoryInfo srcBaseInfo = CopyFileSet.BaseDirectory;
// if source file not specified use fileset
foreach (string pathname in CopyFileSet.FileNames) {
FileInfo srcInfo = new FileInfo(pathname);
if (srcInfo.Exists) {
// will holds the full path to the destination file
string dstFilePath;
if (Flatten) {
dstFilePath = Path.Combine(ToDirectory.FullName,
srcInfo.Name);
} else {
// Gets the relative path and file info from the full
// source filepath
// pathname = C:\f2\f3\file1, srcBaseInfo=C:\f2, then
// dstRelFilePath=f3\file1
string dstRelFilePath = "";
if (srcInfo.FullName.IndexOf(srcBaseInfo.FullName, 0) != -1) {
dstRelFilePath = srcInfo.FullName.Substring(
srcBaseInfo.FullName.Length);
} else {
dstRelFilePath = srcInfo.Name;
}
if (dstRelFilePath[0] == Path.DirectorySeparatorChar) {
dstRelFilePath = dstRelFilePath.Substring(1);
}
// The full filepath to copy to.
dstFilePath = Path.Combine(ToDirectory.FullName,
dstRelFilePath);
}
// do the outdated check
FileInfo dstInfo = new FileInfo(dstFilePath);
bool outdated = (!dstInfo.Exists) || (srcInfo.LastWriteTime > dstInfo.LastWriteTime);
if (Overwrite || outdated) {
// construct FileDateInfo for current file
FileDateInfo newFile = new FileDateInfo(srcInfo.FullName,
srcInfo.LastWriteTime);
// if multiple source files are selected to be copied
// to the same destination file, then only the last
// updated source should actually be copied
FileDateInfo oldFile = (FileDateInfo) FileCopyMap[dstInfo.FullName];
if (oldFile != null) {
// if current file was updated after scheduled file,
// then replace it
if (newFile.LastWriteTime > oldFile.LastWriteTime) {
FileCopyMap[dstInfo.FullName] = newFile;
}
} else {
FileCopyMap.Add(dstInfo.FullName, newFile);
if (dstInfo.Exists && dstInfo.Attributes != FileAttributes.Normal) {
File.SetAttributes(dstInfo.FullName, FileAttributes.Normal);
}
}
}
} else {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"Could not find file '{0}' to copy.", srcInfo.FullName),
Location);
}
}
if (IncludeEmptyDirs && !Flatten) {
// create any specified directories that weren't created during the copy (ie: empty directories)
foreach (string pathname in CopyFileSet.DirectoryNames) {
DirectoryInfo srcInfo = new DirectoryInfo(pathname);
// skip directory if not relative to base dir of fileset
if (srcInfo.FullName.IndexOf(srcBaseInfo.FullName) == -1) {
continue;
}
string dstRelPath = srcInfo.FullName.Substring(srcBaseInfo.FullName.Length);
if (dstRelPath.Length > 0 && dstRelPath[0] == Path.DirectorySeparatorChar) {
dstRelPath = dstRelPath.Substring(1);
}
// The full filepath to copy to.
string destinationDirectory = Path.Combine(ToDirectory.FullName, dstRelPath);
if (!Directory.Exists(destinationDirectory)) {
try {
Directory.CreateDirectory(destinationDirectory);
} catch (Exception ex) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"Failed to create directory '{0}'.", destinationDirectory ),
Location, ex);
}
Log(Level.Verbose, "Created directory '{0}'.", destinationDirectory);
}
}
}
}
// do all the actual copy operations now
DoFileOperations();
}
#endregion Override implementation of Task
#region Protected Instance Methods
///
/// Actually does the file copies.
///
protected virtual void DoFileOperations() {
int fileCount = FileCopyMap.Count;
if (fileCount > 0 || Verbose) {
if (ToFile != null) {
Log(Level.Info, "Copying {0} file{1} to '{2}'.", fileCount, (fileCount != 1) ? "s" : "", ToFile);
} else {
Log(Level.Info, "Copying {0} file{1} to '{2}'.", fileCount, (fileCount != 1) ? "s" : "", ToDirectory);
}
// loop thru our file list
foreach (DictionaryEntry fileEntry in FileCopyMap) {
string destinationFile = (string) fileEntry.Key;
string sourceFile = ((FileDateInfo) fileEntry.Value).Path;
if (sourceFile == destinationFile) {
Log(Level.Verbose, "Skipping self-copy of '{0}'.", sourceFile);
continue;
}
try {
Log(Level.Verbose, "Copying '{0}' to '{1}'.", sourceFile, destinationFile);
// create directory if not present
string destinationDirectory = Path.GetDirectoryName(destinationFile);
if (!Directory.Exists(destinationDirectory)) {
Directory.CreateDirectory(destinationDirectory);
Log(Level.Verbose, "Created directory '{0}'.", destinationDirectory);
}
// copy the file with filters
FileUtils.CopyFile(sourceFile, destinationFile, Filters,
InputEncoding, OutputEncoding);
} catch (Exception ex) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"Cannot copy '{0}' to '{1}'.", sourceFile, destinationFile),
Location, ex);
}
}
}
}
#endregion Protected Instance Methods
///
/// Holds the absolute paths and last write time of a given file.
///
protected class FileDateInfo {
#region Public Instance Constructors
///
/// Initializes a new instance of the
/// class for the specified file and last write time.
///
/// The absolute path of the file.
/// The last write time of the file.
public FileDateInfo(string path, DateTime lastWriteTime) {
_path = path;
_lastWriteTime = lastWriteTime;
}
#endregion Public Instance Constructors
#region Public Instance Properties
///
/// Gets the absolute path of the current file.
///
///
/// The absolute path of the current file.
///
public string Path {
get { return _path; }
}
///
/// Gets the time when the current file was last written to.
///
///
/// The time when the current file was last written to.
///
public DateTime LastWriteTime {
get { return _lastWriteTime; }
}
#endregion Public Instance Properties
#region Private Instance Fields
private DateTime _lastWriteTime;
private string _path;
#endregion Private Instance Fields
}
}
}