* Copyright 2005-2009, Ingo Weinhold, ingo_weinhold@gmx.de.
* Distributed under the terms of the MIT License.
*/
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <fs_attr.h>
#include <Mime.h>
#include <Node.h>
#include <Path.h>
#include <SymLink.h>
#include <TypeConstants.h>
#include <EntryFilter.h>
using BPrivate::EntryFilter;
static const char *kCommandName = "copyattr";
static const int kCopyBufferSize = 64 * 1024;
static int kArgc;
static const char *const *kArgv;
const char *kUsage =
"Usage: %s <options> <source> [ ... ] <destination>\n"
"\n"
"Copies attributes from one or more files to another, or copies one or more\n"
"files or directories, with all or a specified subset of their attributes, to\n"
"another location.\n"
"\n"
"If option \"-d\"/\"--data\" is given, the behavior is similar to \"cp -df\",\n"
"save that attributes are copied. That is, if more than one source file is\n"
"given, the destination file must be a directory. If the destination is a\n"
"directory (or a symlink to a directory), the source files are copied into\n"
"the destination directory. Entries that are in the way are removed, unless\n"
"they are directories. If the source is a directory too, the attributes will\n"
"be copied and, if recursive operation is specified, the program continues\n"
"copying the contents of the source directory. If the source is not a\n"
"directory the program aborts with an error message.\n"
"\n"
"If option \"-d\"/\"--data\" is not given, only attributes are copied.\n"
"Regardless of the file type of the destination, the attributes of the source\n"
"files are copied to it. If an attribute with the same name as one to be\n"
"copied already exists, it is replaced. If more than one source file is\n"
"specified the semantics are similar to invoking the program multiple times\n"
"with the same options and destination and only one source file at a time,\n"
"in the order the source files are given. If recursive operation is\n"
"specified, the program recursively copies the attributes of the directory\n"
"contents; if the destination file is not a directory, or for a source entry\n"
"there exists no destination entry, the program aborts with an error\n"
"message.\n"
"\n"
"Note, that the behavior of the program differs from the one shipped with\n"
"BeOS R5.\n"
"\n"
"Options:\n"
" -d, --data - Copy the data of the file(s), too.\n"
" -h, --help - Print this help text and exit.\n"
" -m, --move - If -d is given, the source files are removed after\n"
" being copied. Has no effect otherwise.\n"
" -n, --name <name> - Only copy the attribute with name <name>.\n"
" -r, --recursive - Copy directories recursively.\n"
" -t, --type <type> - Copy only the attributes of type <type>. If -n is\n"
" specified too, only the attribute matching the name\n"
" and the type is copied.\n"
" -x <pattern> - Exclude source entries matching <pattern>.\n"
" -X <pattern> - Exclude source paths matching <pattern>.\n"
" -v, --verbose - Print more messages.\n"
" -, -- - Marks the end of options. The arguments after, even\n"
" if starting with \"-\" are considered file names.\n"
"\n"
"Parameters:\n"
" <type> - One of: int, llong, string, mimestr, float, double,\n"
" boolean.\n"
;
struct supported_attribute_type {
const char *type_name;
type_code type;
};
const supported_attribute_type kSupportedAttributeTypes[] = {
{ "int", B_INT32_TYPE },
{ "llong", B_INT64_TYPE },
{ "string", B_STRING_TYPE },
{ "mimestr", B_MIME_STRING_TYPE },
{ "float", B_FLOAT_TYPE },
{ "double", B_DOUBLE_TYPE },
{ "boolean", B_BOOL_TYPE },
{ NULL, 0 },
};
struct AttributeFilter {
AttributeFilter()
:
fName(NULL),
fType(B_ANY_TYPE)
{
}
void SetTo(const char *name, type_code type)
{
fName = name;
fType = type;
}
bool Filter(const char *name, type_code type) const {
if (fName && strcmp(name, fName) != 0)
return false;
return (fType == B_ANY_TYPE || type == fType);
}
private:
const char *fName;
type_code fType;
};
struct Parameters {
Parameters()
:
copy_data(false),
recursive(false),
move_files(false),
verbose(false)
{
}
bool copy_data;
bool recursive;
bool move_files;
bool verbose;
AttributeFilter attribute_filter;
EntryFilter entry_filter;
};
static void
print_usage(bool error)
{
const char *commandName = NULL;
if (kArgc > 0) {
if (const char *lastSlash = strchr(kArgv[0], '/'))
commandName = lastSlash + 1;
else
commandName = kArgv[0];
}
if (!commandName || strlen(commandName) == 0)
commandName = kCommandName;
fprintf((error ? stderr : stdout), kUsage, commandName);
}
static void
print_usage_and_exit(bool error)
{
print_usage(error);
exit(error ? 1 : 0);
}
static const char *
next_arg(int &argi, bool optional = false)
{
if (argi >= kArgc) {
if (!optional)
print_usage_and_exit(true);
return NULL;
}
return kArgv[argi++];
}
static void
copy_attributes(const char *sourcePath, BNode &source, const char *destPath,
BNode &destination, const Parameters ¶meters)
{
char attrName[B_ATTR_NAME_LENGTH];
while (source.GetNextAttrName(attrName) == B_OK) {
attr_info attrInfo;
status_t error = source.GetAttrInfo(attrName, &attrInfo);
if (error != B_OK) {
fprintf(stderr, "Error: Failed to get info of attribute \"%s\" "
"of file \"%s\": %s\n", attrName, sourcePath, strerror(error));
exit(1);
}
if (!parameters.attribute_filter.Filter(attrName, attrInfo.type))
continue;
char buffer[kCopyBufferSize];
off_t offset = 0;
off_t bytesLeft = attrInfo.size;
do {
size_t toRead = kCopyBufferSize;
if ((off_t)toRead > bytesLeft)
toRead = bytesLeft;
ssize_t bytesRead = source.ReadAttr(attrName, attrInfo.type,
offset, buffer, toRead);
if (bytesRead < 0) {
fprintf(stderr, "Error: Failed to read attribute \"%s\" "
"of file \"%s\": %s\n", attrName, sourcePath,
strerror(bytesRead));
exit(1);
}
ssize_t bytesWritten = destination.WriteAttr(attrName,
attrInfo.type, offset, buffer, bytesRead);
if (bytesWritten < 0) {
fprintf(stderr, "Error: Failed to write attribute \"%s\" "
"of file \"%s\": %s\n", attrName, destPath,
strerror(bytesWritten));
exit(1);
}
bytesLeft -= bytesRead;
offset += bytesRead;
} while (bytesLeft > 0);
}
}
static void
copy_file_data(const char *sourcePath, BFile &source, const char *destPath,
BFile &destination, const Parameters ¶meters)
{
char buffer[kCopyBufferSize];
off_t offset = 0;
while (true) {
ssize_t bytesRead = source.ReadAt(offset, buffer, sizeof(buffer));
if (bytesRead < 0) {
fprintf(stderr, "Error: Failed to read from file \"%s\": %s\n",
sourcePath, strerror(bytesRead));
exit(1);
}
if (bytesRead == 0)
return;
ssize_t bytesWritten = destination.WriteAt(offset, buffer, bytesRead);
if (bytesWritten < 0) {
fprintf(stderr, "Error: Failed to write to file \"%s\": %s\n",
destPath, strerror(bytesWritten));
exit(1);
}
offset += bytesRead;
}
}
static void
copy_entry(const char *sourcePath, const char *destPath,
const Parameters ¶meters)
{
if (!parameters.entry_filter.Filter(sourcePath))
return;
struct stat sourceStat;
if (lstat(sourcePath, &sourceStat) < 0) {
fprintf(stderr, "Error: Couldn't access \"%s\": %s\n", sourcePath,
strerror(errno));
exit(1);
}
struct stat destStat;
bool destExists = lstat(destPath, &destStat) == 0;
if (!destExists && !parameters.copy_data) {
fprintf(stderr, "Error: Destination file \"%s\" does not exist.\n",
destPath);
exit(1);
}
if (parameters.verbose)
printf("%s\n", destPath);
bool unlinkDest = (destExists && parameters.copy_data);
bool createDest = parameters.copy_data;
if (destExists) {
if (S_ISDIR(destStat.st_mode)) {
if (S_ISDIR(sourceStat.st_mode)) {
unlinkDest = false;
createDest = false;
} else if (parameters.copy_data || parameters.recursive) {
fprintf(stderr, "Error: Can't copy \"%s\", since directory "
"\"%s\" is in the way.\n", sourcePath, destPath);
exit(1);
}
}
}
if (unlinkDest) {
if (unlink(destPath) < 0) {
fprintf(stderr, "Error: Failed to unlink \"%s\": %s\n", destPath,
strerror(errno));
exit(1);
}
}
BNode _sourceNode;
BFile sourceFile;
BDirectory sourceDir;
BNode *sourceNode = NULL;
status_t error;
if (S_ISDIR(sourceStat.st_mode)) {
error = sourceDir.SetTo(sourcePath);
sourceNode = &sourceDir;
} else if (S_ISREG(sourceStat.st_mode)) {
error = sourceFile.SetTo(sourcePath, B_READ_ONLY);
sourceNode = &sourceFile;
} else {
error = _sourceNode.SetTo(sourcePath);
sourceNode = &_sourceNode;
}
if (error != B_OK) {
fprintf(stderr, "Error: Failed to open \"%s\": %s\n",
sourcePath, strerror(error));
exit(1);
}
BNode _destNode;
BDirectory destDir;
BFile destFile;
BSymLink destSymLink;
BNode *destNode = NULL;
if (createDest) {
if (S_ISDIR(sourceStat.st_mode)) {
error = BDirectory().CreateDirectory(destPath, &destDir);
if (error != B_OK) {
fprintf(stderr, "Error: Failed to make directory \"%s\": %s\n",
destPath, strerror(error));
exit(1);
}
destNode = &destDir;
} else if (S_ISREG(sourceStat.st_mode)) {
error = BDirectory().CreateFile(destPath, &destFile);
if (error != B_OK) {
fprintf(stderr, "Error: Failed to create file \"%s\": %s\n",
destPath, strerror(error));
exit(1);
}
destNode = &destFile;
copy_file_data(sourcePath, sourceFile, destPath, destFile,
parameters);
} else if (S_ISLNK(sourceStat.st_mode)) {
char linkTo[B_PATH_NAME_LENGTH + 1];
ssize_t bytesRead = readlink(sourcePath, linkTo,
sizeof(linkTo) - 1);
if (bytesRead < 0) {
fprintf(stderr, "Error: Failed to read symlink \"%s\": %s\n",
sourcePath, strerror(errno));
exit(1);
}
linkTo[bytesRead] = '\0';
error = BDirectory().CreateSymLink(destPath, linkTo, &destSymLink);
if (error != B_OK) {
fprintf(stderr, "Error: Failed to create symlink \"%s\": %s\n",
destPath, strerror(error));
exit(1);
}
destNode = &destSymLink;
} else {
fprintf(stderr, "Error: Source file \"%s\" has unsupported type.\n",
sourcePath);
exit(1);
}
copy_attributes(sourcePath, *sourceNode, destPath, *destNode,
parameters);
destNode->SetOwner(sourceStat.st_uid);
destNode->SetGroup(sourceStat.st_gid);
destNode->SetPermissions(sourceStat.st_mode);
#ifdef HAIKU_TARGET_PLATFORM_HAIKU
destNode->SetCreationTime(sourceStat.st_crtime);
#endif
destNode->SetModificationTime(sourceStat.st_mtime);
} else {
error = _destNode.SetTo(destPath);
if (error != B_OK) {
fprintf(stderr, "Error: Failed to open \"%s\": %s\n",
destPath, strerror(error));
exit(1);
}
destNode = &_destNode;
copy_attributes(sourcePath, *sourceNode, destPath, *destNode,
parameters);
}
destNode->Unset();
if (parameters.recursive && S_ISDIR(sourceStat.st_mode)) {
char buffer[offsetof(struct dirent, d_name) + B_FILE_NAME_LENGTH];
dirent *entry = (dirent*)buffer;
while (sourceDir.GetNextDirents(entry, sizeof(buffer), 1) == 1) {
if (strcmp(entry->d_name, ".") == 0
|| strcmp(entry->d_name, "..") == 0) {
continue;
}
BPath sourceEntryPath;
error = sourceEntryPath.SetTo(sourcePath, entry->d_name);
if (error != B_OK) {
fprintf(stderr, "Error: Failed to construct entry path from "
"dir \"%s\" and name \"%s\": %s\n",
sourcePath, entry->d_name, strerror(error));
exit(1);
}
BPath destEntryPath;
error = destEntryPath.SetTo(destPath, entry->d_name);
if (error != B_OK) {
fprintf(stderr, "Error: Failed to construct entry path from "
"dir \"%s\" and name \"%s\": %s\n",
destPath, entry->d_name, strerror(error));
exit(1);
}
copy_entry(sourceEntryPath.Path(), destEntryPath.Path(),
parameters);
}
}
if (parameters.move_files) {
if (S_ISDIR(sourceStat.st_mode)) {
if (rmdir(sourcePath) < 0) {
fprintf(stderr, "Error: Failed to remove \"%s\": %s\n",
sourcePath, strerror(errno));
exit(1);
}
} else {
if (unlink(sourcePath) < 0) {
fprintf(stderr, "Error: Failed to unlink \"%s\": %s\n",
sourcePath, strerror(errno));
exit(1);
}
}
}
}
static void
copy_files(const char **sourcePaths, int sourceCount,
const char *destPath, const Parameters ¶meters)
{
BEntry destEntry;
status_t error = destEntry.SetTo(destPath);
if (error != B_OK) {
fprintf(stderr, "Error: Couldn't access \"%s\": %s\n", destPath,
strerror(error));
exit(1);
}
bool destExists = destEntry.Exists();
bool destIsDir = false;
if (destExists && parameters.copy_data) {
struct stat st;
error = destEntry.GetStat(&st);
if (error != B_OK) {
fprintf(stderr, "Error: Failed to stat \"%s\": %s\n", destPath,
strerror(error));
exit(1);
}
if (S_ISDIR(st.st_mode)) {
destIsDir = true;
} else if (S_ISLNK(st.st_mode)) {
BEntry resolvedDestEntry;
if (resolvedDestEntry.SetTo(destPath, true) == B_OK
&& resolvedDestEntry.IsDirectory()) {
destIsDir = true;
}
}
}
if (sourceCount > 1 && parameters.copy_data && !destIsDir) {
fprintf(stderr, "Error: Destination needs to be a directory when "
"multiple source files are specified and option \"-d\" is "
"given.\n");
exit(1);
}
for (int i = 0; i < sourceCount; i++) {
const char *sourcePath = sourcePaths[i];
bool copySourceContentsOnly = false;
if (destIsDir) {
int sourceLen = strlen(sourcePath);
while (sourceLen > 1 && sourcePath[sourceLen - 1] == '/')
sourceLen--;
int leafStart = sourceLen;
while (leafStart > 0 && sourcePath[leafStart - 1] != '/')
leafStart--;
int leafLen = sourceLen - leafStart;
if (leafLen == 0 || (leafLen <= 2
&& strncmp(sourcePath + leafStart, "..", leafLen) == 0)) {
copySourceContentsOnly = true;
}
}
if (destIsDir && !copySourceContentsOnly) {
BPath normalizedSourcePath;
error = normalizedSourcePath.SetTo(sourcePath);
if (error != B_OK) {
fprintf(stderr, "Error: Invalid path \"%s\".\n", sourcePath);
exit(1);
}
BPath destEntryPath;
error = destEntryPath.SetTo(destPath, normalizedSourcePath.Leaf());
if (error != B_OK) {
fprintf(stderr, "Error: Failed to get destination path for "
"source \"%s\" and destination directory \"%s\".\n",
sourcePath, destPath);
exit(1);
}
copy_entry(normalizedSourcePath.Path(), destEntryPath.Path(),
parameters);
} else {
copy_entry(sourcePath, destPath, parameters);
}
}
}
int
main(int argc, const char *const *argv)
{
kArgc = argc;
kArgv = argv;
Parameters parameters;
const char *attributeName = NULL;
const char *attributeTypeString = NULL;
const char **files = new const char*[argc];
int fileCount = 0;
bool moreOptions = true;
for (int argi = 1; argi < argc; ) {
const char *arg = argv[argi++];
if (moreOptions && arg[0] == '-') {
if (strcmp(arg, "-d") == 0 || strcmp(arg, "--data") == 0) {
parameters.copy_data = true;
} else if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) {
print_usage_and_exit(false);
} else if (strcmp(arg, "-m") == 0 || strcmp(arg, "--move") == 0) {
parameters.move_files = true;
} else if (strcmp(arg, "-n") == 0 || strcmp(arg, "--name") == 0) {
if (attributeName) {
fprintf(stderr, "Error: Only one attribute name can be "
"specified.\n");
exit(1);
}
attributeName = next_arg(argi);
} else if (strcmp(arg, "-r") == 0
|| strcmp(arg, "--recursive") == 0) {
parameters.recursive = true;
} else if (strcmp(arg, "-t") == 0 || strcmp(arg, "--type") == 0) {
if (attributeTypeString) {
fprintf(stderr, "Error: Only one attribute type can be "
"specified.\n");
exit(1);
}
attributeTypeString = next_arg(argi);
} else if (strcmp(arg, "-v") == 0
|| strcmp(arg, "--verbose") == 0) {
parameters.verbose = true;
} else if (strcmp(arg, "-x") == 0) {
parameters.entry_filter.AddExcludeFilter(next_arg(argi), true);
} else if (strcmp(arg, "-X") == 0) {
parameters.entry_filter.AddExcludeFilter(next_arg(argi), false);
} else if (strcmp(arg, "-") == 0 || strcmp(arg, "--") == 0) {
moreOptions = false;
} else {
fprintf(stderr, "Error: Invalid option: \"%s\"\n", arg);
print_usage_and_exit(true);
}
} else {
files[fileCount++] = arg;
}
}
if (fileCount < 2) {
fprintf(stderr, "Error: Not enough file names specified.\n");
print_usage_and_exit(true);
}
type_code attributeType = B_ANY_TYPE;
if (attributeTypeString) {
bool found = false;
for (int i = 0; kSupportedAttributeTypes[i].type_name; i++) {
if (strcmp(attributeTypeString,
kSupportedAttributeTypes[i].type_name) == 0) {
found = true;
attributeType = kSupportedAttributeTypes[i].type;
break;
}
}
if (!found) {
fprintf(stderr, "Error: Unsupported attribute type: \"%s\"\n",
attributeTypeString);
exit(1);
}
}
parameters.attribute_filter.SetTo(attributeName, attributeType);
parameters.move_files &= parameters.copy_data;
fileCount--;
const char *destination = files[fileCount];
files[fileCount] = NULL;
copy_files(files, fileCount, destination, parameters);
delete[] files;
return 0;
}