syncfiles/sync.c

839 lines
20 KiB
C

#include "defs.h"
#include <Files.h>
#include <Folders.h>
#include <MacMemory.h>
#include <StringCompare.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
typedef enum {
kLogWarn,
kLogInfo,
kLogVerbose,
} log_level;
static log_level gLogLevel = kLogInfo;
enum {
// Maximum file size that we will copy.
kMaxFileSize = 64 * 1024,
};
typedef int (*convert_func)(unsigned char **outptr, unsigned char *outend,
const unsigned char **inptr,
const unsigned char *inend);
void print_err(const char *msg, ...) {
va_list ap;
fputs("## Error: ", stderr);
va_start(ap, msg);
vfprintf(stderr, msg, ap);
va_end(ap);
fputc('\n', stderr);
}
// Map from OSErr codes to messages. As a heuristic, this should include error
// codes caused by toolbox calls in this program which are readily understood by
// the user. It can exclude error codes that are likely to indicate a
// programming error.
struct error_message {
OSErr err;
const char *msg;
};
#define E(e, m) \
{ e, m "\0" #e }
const struct error_message kErrorMessages[] = {
E(dirFulErr, "directory full"), // -33
E(dskFulErr, "disk full"), // -34
E(ioErr, "I/O error"), // -36
E(bdNamErr, "bad name"), // -37
E(fnfErr, "file not found"), // -43
E(wPrErr, "disk is write-protected"), // -44
E(fLckdErr, "file is locked"), // -45
E(vLckdErr, "volume is locked"), // -46
E(fBsyErr, "file is busy"), // -47
E(dupFNErr, "destination already exists"), // -48
E(opWrErr, "file already open for writing"), // -49
E(paramErr, "parameter error"), // -50
E(permErr, "cannot write to locked file"), // -54
E(dirNFErr, "directory not found"), // -120
E(wrgVolTypErr, "not an HFS volume"), // -123
E(diffVolErr, "files on different volumes"), // -1303
E(afpAccessDenied, "user does not have access privileges (AFP)"), // -5000
E(afpObjectTypeErr,
"file/directory specified where directory/file expected"), // -5025
E(afpSameObjectErr, "objects are the same"), // -5038
};
static const char *mac_strerror(OSErr err) {
int i, n = sizeof(kErrorMessages) / sizeof(*kErrorMessages);
for (i = 0; i < n; i++) {
if (kErrorMessages[i].err == err) {
return kErrorMessages[i].msg;
}
}
return NULL;
}
void print_errcode(int err, const char *msg, ...) {
va_list ap;
const char *emsg;
fputs("## Error: ", stderr);
va_start(ap, msg);
vfprintf(stderr, msg, ap);
va_end(ap);
emsg = mac_strerror(err);
if (emsg != NULL) {
fprintf(stderr, ": %s (%d)\n", emsg, err);
} else {
fprintf(stderr, ": err=%d\n", err);
}
}
static void log_call(int err, const char *function) {
const char *emsg;
if (err == 0) {
fprintf(stderr, "## %s: noErr\n", function);
return;
}
emsg = mac_strerror(err);
if (emsg != NULL) {
emsg += strlen(emsg) + 1;
fprintf(stderr, "## %s: %s (%d)\n", function, emsg, err);
} else {
fprintf(stderr, "## %s: %d\n", function, err);
}
}
// Convert a C to Pascal string.
static int c2pstr(Str255 ostr, const char *istr) {
size_t n = strlen(istr);
if (n > 255) {
print_err("path too long: %s", istr);
return 1;
}
ostr[0] = n;
memcpy(ostr + 1, istr, n);
memset(ostr + 1 + n, 0, 255 - n);
return 0;
}
// Convert a Pascal to C string.
static void p2cstr(char *ostr, const unsigned char *istr) {
unsigned len = istr[0];
memcpy(ostr, istr + 1, len);
ostr[len] = '\0';
}
struct file_meta {
Boolean exists;
long modTime;
};
enum {
kSrcDir,
kDestDir,
};
enum {
kModeAuto,
kModePush,
kModePull,
};
struct file_info {
Str31 name;
struct file_meta meta[2];
int mode;
};
// Array of metadata entries for files.
static Handle gFiles;
static unsigned gFileCount;
static unsigned gFileAlloc;
// Get the metadata entry for a file with the given name.
static struct file_info *get_file(const unsigned char *name) {
unsigned i, n = gFileCount, len, newAlloc;
struct file_info *file, *array;
Handle newFiles;
OSErr err;
len = name[0];
if (len > 31) {
fputs("## Error: name too long\n", stderr);
return NULL;
}
if (gFiles == NULL) {
newAlloc = 8;
newFiles = NewHandle(newAlloc * sizeof(struct file_info));
err = MemError();
if (err != noErr) {
fputs("## Error: out of memory\n", stderr);
return NULL;
}
gFiles = newFiles;
gFileAlloc = newAlloc;
} else {
array = (struct file_info *)*gFiles;
for (i = 0; i < n; i++) {
file = &array[i];
if (memcmp(file->name, name, len + 1) == 0) {
return file;
}
}
if (n >= gFileAlloc) {
newAlloc = n * 2;
SetHandleSize(gFiles, newAlloc * sizeof(struct file_info));
err = MemError();
if (err != noErr) {
fputs("## Error: out of memory\n", stderr);
return NULL;
}
gFileAlloc = newAlloc;
}
}
array = (struct file_info *)*gFiles;
file = &array[n];
gFileCount = n + 1;
memset(file, 0, sizeof(*file));
memcpy(file->name, name, len + 1);
return file;
}
// Get the volume and directory ID for a directory from its path.
static int dir_from_path(short *vRefNum, long *dirID, const char *dirpath) {
Str255 ppath;
FSSpec spec;
CInfoPBRec ci;
OSErr err;
if (c2pstr(ppath, dirpath)) {
return 1;
}
err = FSMakeFSSpec(0, 0, ppath, &spec);
if (err != 0) {
if (err == fnfErr) {
print_err("does not exist: %s", dirpath);
} else {
print_errcode(err, "FSMakeFSSpec");
}
return 1;
}
memset(&ci, 0, sizeof(ci));
ci.dirInfo.ioNamePtr = spec.name;
ci.dirInfo.ioVRefNum = spec.vRefNum;
ci.dirInfo.ioDrDirID = spec.parID;
err = PBGetCatInfoSync(&ci);
if (err != 0) {
print_errcode(err, "PBGetCatInfoSync");
return 1;
}
if ((ci.dirInfo.ioFlAttrib & kioFlAttribDirMask) == 0) {
print_err("not a directory: %s", dirpath);
return 1;
}
*vRefNum = ci.dirInfo.ioVRefNum;
*dirID = ci.dirInfo.ioDrDirID;
return 0;
}
// Return true if a file with the given name should be included. The name is a
// Pascal string.
static int filter_name(const unsigned char *name) {
int len, i, stem;
const unsigned char *ext;
char temp[32];
if (EqualString(name, "\pmakefile", FALSE, TRUE)) {
return 1;
}
stem = 0;
len = name[0];
for (i = 1; i <= len; i++) {
if (name[i] == '.') {
stem = i;
}
}
if (stem != 0) {
ext = name + stem + 1;
switch (len - stem) {
case 1:
if (ext[0] == 'c' || ext[0] == 'h' || ext[0] == 'r') {
return 1;
}
break;
}
}
if (gLogLevel >= kLogVerbose) {
p2cstr(temp, name);
fprintf(stderr, "## Ignored: %s\n", temp);
}
return 0;
}
// List files in a directory, filter them, and add the files matching the filter
// to the file list. The value of 'which' should be kSrcDir or kDestDir.
static int list_dir(short vRefNum, long dirID, int which) {
Str255 ppath;
CInfoPBRec ci;
struct file_info *file;
OSErr err;
int i;
for (i = 1; i < 100; i++) {
memset(&ci, 0, sizeof(ci));
ci.dirInfo.ioNamePtr = ppath;
ci.dirInfo.ioVRefNum = vRefNum;
ci.dirInfo.ioDrDirID = dirID;
ci.dirInfo.ioFDirIndex = i;
err = PBGetCatInfoSync(&ci);
if (err != 0) {
if (err == fnfErr) {
break;
}
print_errcode(err, "could not list directory");
continue;
}
if ((ci.hFileInfo.ioFlAttrib & kioFlAttribDirMask) == 0 &&
filter_name(ppath)) {
ppath[ppath[0] + 1] = '\0';
file = get_file(ppath);
file->meta[which].exists = TRUE;
file->meta[which].modTime = ci.hFileInfo.ioFlMdDat;
}
}
return 0;
}
// Read the entire data fork of a file. The result must be freed with
// DisposePtr.
static int read_file(FSSpec *spec, Ptr *data, long *length) {
CInfoPBRec ci;
Ptr ptr;
long dataLength, pos, count;
OSErr err;
short fref;
// Get file size.
memset(&ci, 0, sizeof(ci));
ci.hFileInfo.ioNamePtr = spec->name;
ci.hFileInfo.ioVRefNum = spec->vRefNum;
ci.hFileInfo.ioDirID = spec->parID;
err = PBGetCatInfoSync(&ci);
if (err != 0) {
print_errcode(err, "could not get file metadata");
return 1;
}
if ((ci.hFileInfo.ioFlAttrib & kioFlAttribDirMask) != 0) {
print_err("is a directory");
return 1;
}
dataLength = ci.hFileInfo.ioFlLgLen;
if (dataLength > kMaxFileSize) {
print_err("file is too large: size=%ld, max=%ld", dataLength,
kMaxFileSize);
return 1;
}
// Allocate memory.
ptr = NewPtr(dataLength);
err = MemError();
if (err != 0) {
print_errcode(err, "out of memory");
return 1;
}
// Read file.
err = FSpOpenDF(spec, fsRdPerm, &fref);
if (err != 0) {
DisposePtr(ptr);
print_errcode(err, "could not open file");
return 1;
}
pos = 0;
while (pos < dataLength) {
count = dataLength - pos;
err = FSRead(fref, &count, ptr + pos);
if (err != 0) {
DisposePtr(ptr);
FSClose(fref);
print_errcode(err, "could not read file");
return 1;
}
pos += count;
}
FSClose(fref);
*data = ptr;
*length = dataLength;
return 0;
}
// Make an FSSpec for a temporary file.
static int make_temp(FSSpec *temp, short vRefNum, long dirID,
const unsigned char *name) {
Str31 tname;
unsigned pfxlen, maxpfx = 31 - 4;
OSErr err;
pfxlen = name[0];
if (pfxlen > maxpfx) {
pfxlen = maxpfx;
}
memcpy(tname + 1, name + 1, pfxlen);
memcpy(tname + 1 + pfxlen, ".tmp", 4);
tname[0] = pfxlen + 4;
err = FSMakeFSSpec(vRefNum, dirID, tname, temp);
if (err == 0) {
print_err("temporary file exists");
return 1;
} else if (err == fnfErr) {
return 0;
} else {
print_errcode(err, "could not create temp file spec");
return 1;
}
}
// Write the entire contents of a file.
static int write_file(FSSpec *dest, short tempVol, long tempDir, Ptr data,
long length, long modTime, Boolean destExists) {
FSSpec temp;
long pos, amt;
short ref;
HParamBlockRec pb;
CMovePBRec cm;
CInfoPBRec ci;
Str31 name;
OSErr err;
int r;
// Save the data to a temporary file.
r = make_temp(&temp, tempVol, tempDir, dest->name);
if (r != 0) {
return 1;
}
err = FSpCreate(&temp, 'MPS ', 'TEXT', smSystemScript);
if (err != 0) {
print_errcode(err, "could not create file");
return 1;
}
err = FSpOpenDF(&temp, fsRdWrPerm, &ref);
if (err != 0) {
print_errcode(err, "could not open temp file");
goto error;
}
pos = 0;
while (pos < length) {
amt = length - pos;
err = FSWrite(ref, &amt, data + pos);
if (err != 0) {
FSClose(ref);
print_errcode(err, "could not write temp file");
goto error;
}
pos += amt;
}
err = FSClose(ref);
if (err != 0) {
print_errcode(err, "could not close temp file");
goto error;
}
// Update the modification time.
memset(&ci, 0, sizeof(ci));
memcpy(name, temp.name, temp.name[0] + 1);
ci.hFileInfo.ioNamePtr = name;
ci.hFileInfo.ioVRefNum = temp.vRefNum;
ci.hFileInfo.ioDirID = temp.parID;
err = PBGetCatInfoSync(&ci);
if (err != 0) {
print_errcode(err, "could not get temp file info");
goto error;
}
memcpy(name, temp.name, temp.name[0] + 1);
ci.hFileInfo.ioNamePtr = name;
ci.hFileInfo.ioVRefNum = temp.vRefNum;
ci.hFileInfo.ioDirID = temp.parID;
ci.hFileInfo.ioFlMdDat = modTime;
err = PBSetCatInfoSync(&ci);
if (err != 0) {
print_errcode(err, "could not set temp file info");
goto error;
}
// First, try to exchange files if destination exists.
if (destExists) {
err = FSpExchangeFiles(&temp, dest);
if (gLogLevel >= kLogVerbose) {
log_call(err, "FSpExchangeFiles");
}
if (err == 0) {
err = FSpDelete(&temp);
if (err != 0) {
print_errcode(err, "could not remove temporary file");
return 1;
}
return 0;
}
// paramErr: function not supported by volume.
if (err != paramErr) {
print_errcode(err, "could not delete temp file");
return 1;
}
if (gLogLevel >= kLogVerbose) {
fputs("## FSpExchangeFiles not supported\n", stderr);
}
// Otherwise, delete destination and move temp file over.
err = FSpDelete(dest);
if (err != 0) {
print_errcode(err, "could not remove destination file");
goto error;
}
}
// Next, try MoveRename.
memset(&pb, 0, sizeof(pb));
pb.copyParam.ioNamePtr = temp.name;
pb.copyParam.ioVRefNum = temp.vRefNum;
pb.copyParam.ioNewName = dest->name;
pb.copyParam.ioNewDirID = dest->parID;
pb.copyParam.ioDirID = temp.parID;
err = PBHMoveRenameSync(&pb);
if (gLogLevel >= kLogVerbose) {
log_call(err, "PBHMoveRename");
}
if (err == 0) {
return 0;
}
// paramErr: function not supported by volume.
if (err != paramErr) {
print_errcode(err, "could not rename temporary file");
goto error;
}
// Finally, try move and then rename.
if (dest->parID != temp.parID) {
if (gLogLevel >= kLogVerbose) {
fputs("## PBCatMoveSync\n", stderr);
}
memset(&cm, 0, sizeof(cm));
cm.ioNamePtr = temp.name;
cm.ioVRefNum = temp.vRefNum;
cm.ioNewDirID = dest->parID;
cm.ioDirID = temp.parID;
err = PBCatMoveSync(&cm);
if (gLogLevel >= kLogVerbose) {
log_call(err, "PBCatMove");
}
if (err != 0) {
print_errcode(err, "could not move temporary file");
goto error;
}
temp.parID = dest->parID;
}
if (memcmp(dest->name, temp.name, dest->name[0] + 1) != 0) {
if (gLogLevel >= kLogVerbose) {
fputs("## FSpRename\n", stderr);
}
err = FSpRename(&temp, dest->name);
if (gLogLevel >= kLogVerbose) {
log_call(err, "FSpRename");
}
if (err != 0) {
print_errcode(err, "could not rename temporary file");
goto error;
}
}
return 0;
error:
err = FSpDelete(&temp);
if (err != 0) {
print_errcode(err, "could not delete temp file");
}
return 1;
}
// Copy the source file to the destination file. A temporary file is created in
// the specified temporary directory.
static int sync_file(struct file_info *file, convert_func func, long srcVol,
short srcDir, long destVol, short destDir, long tempVol,
short tempDir, long modTime) {
FSSpec src, dest;
Ptr srcData = NULL, destData = NULL;
long srcLength, destLength;
int r;
OSErr err;
unsigned char *outptr, *outend;
const unsigned char *inptr, *inend;
Boolean destExists;
// Create file specs.
err = FSMakeFSSpec(srcVol, srcDir, file->name, &src);
if (err != 0) {
print_errcode(err, "could not create source spec");
return 1;
}
err = FSMakeFSSpec(destVol, destDir, file->name, &dest);
if (err == 0) {
destExists = TRUE;
} else if (err == fnfErr) {
destExists = FALSE;
} else if (err != 0) {
print_errcode(err, "could not create destination spec");
return 1;
}
// Read the source file into memory.
r = read_file(&src, &srcData, &srcLength);
if (r != 0) {
return 1;
}
// Convert data.
destLength = srcLength + (srcLength >> 2) + 16;
destData = NewPtr(destLength);
err = MemError();
if (err != 0) {
print_errcode(err, "out of memory");
goto error;
}
outptr = (unsigned char *)destData;
outend = outptr + destLength;
inptr = (unsigned char *)srcData;
inend = inptr + srcLength;
func(&outptr, outend, &inptr, inend);
if (inptr != inend) {
print_err("conversion function failed");
goto error;
}
destLength = outptr - (unsigned char *)destData;
r = write_file(&dest, tempVol, tempDir, destData, destLength, destExists,
modTime);
if (r != 0) {
goto error;
}
// Clean up.
DisposePtr(srcData);
DisposePtr(destData);
return 0;
error:
if (srcData != NULL) {
DisposePtr(srcData);
}
if (destData != NULL) {
DisposePtr(destData);
}
return 1;
}
static int command_main(char *destpath, int mode) {
short srcVol, destVol, tempVol;
long srcDir, destDir, tempDir;
struct file_info *array, *file, *srcNewer, *destNewer;
OSErr err;
int r, i, j, n;
char name[32];
const char *modeStr;
// Get handles to src and dest directories.
err = HGetVol(NULL, &srcVol, &srcDir);
if (err != 0) {
print_errcode(err, "HGetVol");
return 1;
}
r = dir_from_path(&destVol, &destDir, destpath);
if (r != 0) {
return 1;
}
// List files in src and dest directories.
r = list_dir(srcVol, srcDir, kSrcDir);
if (r != 0) {
return 1;
}
r = list_dir(destVol, destDir, kDestDir);
if (r != 0) {
return 1;
}
if (gFileCount == 0) {
print_err("no files");
return 1;
}
HLock(gFiles);
array = (struct file_info *)*gFiles;
n = gFileCount;
// Figure out the direction for each file.
srcNewer = NULL;
destNewer = NULL;
for (i = 0; i < n; i++) {
file = &array[i];
if (!file->meta[kSrcDir].exists) {
file->mode = kModePull;
destNewer = file;
} else if (!file->meta[kDestDir].exists) {
file->mode = kModePush;
srcNewer = file;
} else if (file->meta[kSrcDir].modTime < file->meta[kDestDir].modTime) {
file->mode = kModePull;
destNewer = file;
} else if (file->meta[kSrcDir].modTime > file->meta[kDestDir].modTime) {
file->mode = kModePush;
srcNewer = file;
}
if (gLogLevel >= kLogVerbose) {
p2cstr(name, file->name);
switch (file->mode) {
default:
case kModeAuto:
modeStr = "equal";
break;
case kModePull:
modeStr = "destNewer";
break;
case kModePush:
modeStr = "srcNewer";
break;
}
fprintf(stderr, "## File: %s %s", name, modeStr);
for (j = 0; j < 2; j++) {
if (!file->meta[j].exists) {
fputs(" -", stderr);
} else {
fprintf(stderr, " %ld", file->meta[j].modTime);
}
}
fputc('\n', stderr);
}
}
// Figure out the mode: push or pull.
if (mode == kModeAuto) {
if (srcNewer != NULL) {
if (destNewer != NULL) {
fputs("## Error: both source and destination have new files\n",
stderr);
p2cstr(name, srcNewer->name);
fprintf(stderr, "## New file in source: %s\n", name);
p2cstr(name, destNewer->name);
fprintf(stderr, "## New file in destination: %s\n", name);
return 1;
}
mode = kModePush;
fputs("## Mode: push\n", stderr);
} else if (destNewer != NULL) {
mode = kModePull;
fputs("## Mode: pull\n", stderr);
} else {
fputs("## No changes.\n", stderr);
return 0;
}
}
// Synchronize the files.
if (mode == kModePull) {
r = mac_from_unix_init();
if (r != 0) {
return 1;
}
}
tempVol = 0;
tempDir = 0;
for (i = 0; i < n; i++) {
file = &array[i];
p2cstr(name, file->name);
if (file->mode == mode) {
if (gLogLevel >= kLogInfo) {
fprintf(stderr, "## Writing %s\n", name);
}
if (mode == kModePush) {
// When pushing, we use the destination directory as the
// temporary folder, to avoid crossing filesystem boundaries on
// the host.
r = sync_file(file, mac_to_unix, srcVol, srcDir, destVol,
destDir, destVol, destDir,
file->meta[kSrcDir].modTime);
} else {
if (tempDir == 0) {
err = FindFolder(srcVol, kTemporaryFolderType, TRUE,
&tempVol, &tempDir);
if (err != 0) {
print_errcode(err, "could not find temporary folder");
return 1;
}
}
r = sync_file(file, mac_to_unix, destVol, destDir, srcVol,
srcDir, tempVol, tempDir,
file->meta[kDestDir].modTime);
}
if (r) {
print_err("failed to copy file: %s", name);
return 1;
}
if (gLogLevel >= kLogVerbose) {
fprintf(stderr, "## Done writing %s\n", name);
}
} else if (file->mode != kModeAuto) {
fprintf(stderr,
"## Warning: Refusing to overwrite '%s', file is newer\n",
name);
}
}
return 0;
}
int main(int argc, char **argv) {
char *destDir = NULL, *arg, *opt;
int i, r, mode = kModeAuto;
for (i = 1; i < argc; i++) {
arg = argv[i];
if (*arg == '-') {
opt = arg + 1;
if (*opt == '-') {
opt++;
}
if (strcmp(opt, "push") == 0) {
mode = kModePush;
} else if (strcmp(opt, "pull") == 0) {
mode = kModePull;
} else if (strcmp(opt, "verbose") == 0 || strcmp(opt, "v") == 0) {
gLogLevel = kLogVerbose;
} else if (strcmp(opt, "quiet") == 0 || strcmp(opt, "q") == 0) {
gLogLevel = kLogWarn;
} else {
print_err("unknown flag: %s", arg);
return 1;
}
} else {
if (destDir != NULL) {
print_err("unexpected argument: %s", arg);
return 1;
}
destDir = arg;
}
}
r = command_main(argv[1], mode);
if (gFiles != NULL) {
DisposeHandle(gFiles);
}
mac_from_unix_term();
if (gLogLevel >= kLogVerbose) {
fputs("## Done\n", stderr);
}
return r;
}