Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
MPFileCopy.c
/* |
File: MPFileCopy.c |
Contains: An HFS Plus APIs copying engine, which optionally uses pre-emptive threads. |
Written by: Quinn |
Copyright: Copyright © 1999 by Apple Computer, Inc., All Rights Reserved. |
You may incorporate this Apple sample source code into your program(s) without |
restriction. This Apple sample source code has been provided "AS IS" and the |
responsibility for its operation is yours. You are not permitted to redistribute |
this Apple sample source code as "Apple sample source code" after having made |
changes. If you're going to re-distribute the source, we require that you make |
it clear in the source that the code was descended from Apple sample source |
code, but that you've made changes. |
Change History (most recent first): |
*/ |
///////////////////////////////////////////////////////////////// |
// MIB Setup |
#include "MoreSetup.h" |
// Mac OS Interfaces |
#include <Files.h> |
#include <Multiprocessing.h> |
#include <Gestalt.h> |
#include <Events.h> |
#include <DriverGestalt.h> |
#include <Navigation.h> |
#include <UTCUtils.h> |
// Standard C interfaces |
#include <stdio.h> |
#include <stddef.h> |
#include <string.h> |
// Wacky C interfaces (-: |
#include <SIOUX.h> |
// Project interfaces |
#include "TradDriverLoaderLib.h" |
#include "MoreInterfaceLib.h" |
///////////////////////////////////////////////////////////////// |
#pragma mark ----- Testing Flags ----- |
// Set kNeverUseMPTasks to true when debugging with a debugger |
// that doesn't handle debugging MP tasks. |
enum { |
kNeverUseMPTasks = false |
}; |
// kSpecialCaseClassicForks controls whether the classic |
// (resource and data) forks are copied by special case code |
// (true) or by the general purpose code (false). This allows |
// you to test the general purpose code on current systems |
// which don't support named forks other than the classic ones. |
enum { |
kSpecialCaseClassicForks = true |
}; |
///////////////////////////////////////////////////////////////// |
#pragma mark ----- Tunable Parameters ----- |
// The following constants control the behaviour of the copy engine. |
// BufferSizeForThisVolumeSpeed |
enum { |
// kDefaultCopyBufferSize = 2L * 1024 * 1024, // Fast be not very responsive. |
kDefaultCopyBufferSize = 256L * 1024, // Slower, but can still use machine. |
kMaximumCopyBufferSize = 2L * 1024 * 1024, |
kMinimumCopyBufferSize = 1024 |
}; |
// CalculateForksToCopy |
enum { |
kExpectedForkCount = 10 // Number of non-classic forks we expect. |
}; |
// CopyFolder |
enum { |
kMaximumFolderDepth = 100, // Arbitrary limit needed for stack allocation, see below. |
kFolderItemsPerBulkCall = 20 // Trades File Manager efficiency versus memory usage, see below. |
}; |
///////////////////////////////////////////////////////////////// |
#pragma mark ----- Utilities ----- |
static OSStatus VolumeContainingFSRef(const FSRef *itemRef, FSVolumeRefNum *volume) |
{ |
OSStatus err; |
FSCatalogInfo catInfo; |
err = FSGetCatalogInfo(itemRef, kFSCatInfoVolume, &catInfo, nil, nil, nil); |
if (err == noErr) { |
*volume = catInfo.volume; |
} |
return err; |
} |
static OSStatus IsFileSharingRunning(Boolean *fileSharingIsRunning) |
// Determines whether File Sharing is running. It does this in |
// the most paranoid way, by checking to see whether the process |
// is running. I might be able to just get away with checking |
// whether the volume is being shared, but that would require |
// determining exactly what's causing the problem. I decided |
// to just be paranoid and say "if the File Sharing process is |
// around, we act as if File Sharing is running". |
{ |
OSStatus err; |
Boolean done; |
ProcessInfoRec processInfo; |
ProcessSerialNumber psn; |
MoreAssertQ(fileSharingIsRunning != nil); |
*fileSharingIsRunning = false; |
// Iterate through the process list. |
psn.highLongOfPSN = 0; |
psn.lowLongOfPSN = kNoProcess; |
done = false; |
do { |
err = GetNextProcess(&psn); |
if (err == noErr) { |
// Get information on this specific process. |
// If the signature matches File Sharing, |
// set *fileSharingIsRunning and leave. |
MoreBlockZero(&processInfo, sizeof(processInfo)); |
processInfo.processInfoLength = sizeof(processInfo); |
err = GetProcessInformation(&psn, &processInfo); |
if (err == noErr && processInfo.processSignature == 'hhgg') { |
*fileSharingIsRunning = true; |
done = true; |
} |
} else if (err == procNotFound) { |
done = true; |
err = noErr; |
} |
} while (err == noErr & ! done); |
return err; |
} |
///////////////////////////////////////////////////////////////// |
#pragma mark ----- Transfer Buffer Size ----- |
static OSStatus GetFileSystemInfo(FSVolumeRefNum vRefNum, |
Boolean *async, |
Boolean *localVolume, |
UInt32 *volumeBytesPerSecond) |
// Returns information about the volume specified by vRefNum. |
// This information allows us to customise our copy engine |
// appropriately. Customisation includes the size of the |
// copy buffer we allocate, whether we test for asynchronicity |
// by calling the low-level driver, and so on. |
{ |
OSStatus err; |
GetVolParmsInfoBuffer volParms; |
HParamBlockRec hpb; |
UInt32 bytesPerSecond; |
hpb.ioParam.ioNamePtr = nil; |
hpb.ioParam.ioVRefNum = vRefNum; |
hpb.ioParam.ioBuffer = (Ptr) &volParms; |
hpb.ioParam.ioReqCount = sizeof(volParms); |
err = PBHGetVolParmsSync(&hpb); |
if (err == noErr) { |
// Version 1 of the GetVolParmsInfoBuffer included the vMAttrib |
// field, so we don't really need to test ioActCount. A noErr |
// result indicates that we have the info we need. This is |
// just a paranoia check. |
MoreAssertQ(hpb.ioParam.ioActCount >= offsetof(GetVolParmsInfoBuffer, vMVolumeGrade)); |
if (async != nil) { |
*async = ((volParms.vMAttrib & (1 << bSupportsAsyncRequests)) != 0); |
} |
if (localVolume != nil) { |
*localVolume = (volParms.vMServerAdr == 0); |
} |
// On the other hand, vMVolumeGrade was not introduced until |
// version 2 of the GetVolParmsInfoBuffer, so we have to explicitly |
// test whether we got a useful value. |
if (volumeBytesPerSecond != nil) { |
bytesPerSecond = 0; |
if (hpb.ioParam.ioActCount >= offsetof(GetVolParmsInfoBuffer, vMForeignPrivID)) { |
if (volParms.vMVolumeGrade <= 0) { |
bytesPerSecond = -volParms.vMVolumeGrade; |
} |
} |
*volumeBytesPerSecond = bytesPerSecond; |
} |
} |
return err; |
} |
static ByteCount BufferSizeForThisVolumeSpeed(UInt32 volumeBytesPerSecond) |
// Calculate an appropriate copy buffer size based on the volumes |
// rated speed. Our target is to use a buffer that takes 0.25 |
// seconds to fill. This is necessary because the volume might be |
// mounted over a very slow link (like ARA), and if we do a 256 KB |
// read over an ARA link we'll block the File Manager queue for |
// so long that other clients (who might have innocently just |
// called PBGetCatInfoSync) will block for a noticable amount of time. |
// |
// Note that volumeBytesPerSecond might be 0, in which case we assume |
// some default value. |
{ |
ByteCount bufferSize; |
if (volumeBytesPerSecond == 0) { |
bufferSize = kDefaultCopyBufferSize; |
} else { |
// We want to issue a single read that takes 0.25 of a second, |
// so devide the bytes per second by 4. |
bufferSize = volumeBytesPerSecond / 4; |
} |
// Round bufferSize down to 512 byte boundary. |
bufferSize &= ~0x01FF; |
// Clip to sensible limits. |
if (bufferSize < kMinimumCopyBufferSize) { |
bufferSize = kMinimumCopyBufferSize; |
} else if (bufferSize > kMaximumCopyBufferSize) { |
bufferSize = kMaximumCopyBufferSize; |
} |
return bufferSize; |
} |
static ByteCount BufferSizeForThisVolume(FSVolumeRefNum vRefNum) |
// This routine calculates the appropriate buffer size for |
// the given vRefNum. It's a simple composition of GetFileSystemInfo |
// BufferSizeForThisVolumeSpeed. |
{ |
ByteCount volumeBytesPerSecond; |
if ( GetFileSystemInfo(vRefNum, nil, nil, &volumeBytesPerSecond) != noErr ) { |
volumeBytesPerSecond = 0; |
} |
return BufferSizeForThisVolumeSpeed(volumeBytesPerSecond); |
} |
///////////////////////////////////////////////////////////////// |
#pragma mark ----- True Async Tests ----- |
// The following code is use to determine whether the File Manager should |
// be called asynchronously or not by testing certain low-level functionality. |
// We do this because certain machines, like the Power Macintosh 6500, which |
// should support asynchronous I/O, don't do so in all circumstances. Instead, |
// all local hard disk I/O is done synchronously. This causes us problems, |
// as described below. |
// |
// When you do synchronous I/O from an MP task, the actual I/O is done |
// asynchronously from a 'software interrupt' (technically known as a |
// SWI, pronounced "swee"). This SWI is just like a hardware |
// interrupt in that it can interrupt the current foreground task at any |
// instruction boundary (as long as the interrupt is not masked). It also |
// (currently) executes with interrupts masked. |
// |
// If the underlying hardware does not work asynchronously, executing |
// asynchronous File Manager I/O from this SWI results in a very poor |
// user experience. Specifically, interrupts remain masked for the duration |
// of the I/O, which causes the mouse to jerk horribly. |
// |
// So, our goal is to not do File Manager operations from MP tasks on machines |
// that have this problem. We detect this in a three stage process, implemented |
// by the IsVolumeTrulyAsync routine. |
// |
// 1. Determine whether the volume's file system is asynchronous. Some |
// external file systems blow up if you call them asynchronously. |
// |
// 2. Determine whether the device driver thinks it runs asynchronously. Some |
// older hard disk device drivers just don't support asynchronous operations. |
// |
// 3. If the volume is local, determine whether the driver is really running |
// asynchronously. This is important on machines like the Power Macintosh |
// 6500, where the device driver is asynchronous but the ATA Manager isn't. |
// We do this by issuing a large PBReadAsync and checking to see whether |
// the I/O completion runs before the PBReadAsync returns. |
// |
// If all three of these checks give the green light on both the source and |
// destination volumes, we run the copying engine from an MP task. If they |
// don't, we do everything from system task level code. |
// |
// Note that somes of these checks aren't possible within a Carbon application. |
// This won't be a problem on Mac OS X (because all drivers are async there) |
// but it could be a problem for Carbon applications on traditional Mac OS. |
// I don't have a good solution to that problem at this time. |
#if !TARGET_API_MAC_CARBON |
// The following code implements part 3 of the true asynchronicity testing |
// algorithm, described above. We |
volatile static Boolean gEarlyCompletion; |
static IOCompletionUPP gMyCompletionUPP; // points to MyCompletion |
static void MyCompletion(ParmBlkPtr paramBlock) |
// The I/O completion routine for the large read request we |
// use for a true test of asynchronicity. |
{ |
#pragma unused(paramBlock) |
gEarlyCompletion = true; |
} |
static OSStatus IsDeviceTrulyAsync(Boolean *async, UInt32 volumeBytesPerSecond, DriverRefNum refNum, SInt16 driveNum) |
// This routine implements part 3 of the true asynchronicity testing |
// algorithm, described above. |
// |
// We make the assumption that the device size is greater |
// than or equal to kMaximumCopyBufferSize, which isn't necessarily |
// true. Should probably revisit that decision at some time. |
{ |
OSStatus err; |
Ptr copyBuffer; |
ParamBlockRec pb; |
ByteCount bufferSize; |
// Calculate what we would consider to be a "large" request. |
bufferSize = BufferSizeForThisVolumeSpeed(volumeBytesPerSecond); |
MoreAssertQ(gMyCompletionUPP != nil); |
// Create the test buffer. |
copyBuffer = NewPtr(bufferSize); |
err = MemError(); |
if (err == noErr) { |
gEarlyCompletion = false; |
// Issue the request. |
pb.ioParam.ioCompletion = gMyCompletionUPP; |
pb.ioParam.ioVRefNum = driveNum; |
pb.ioParam.ioRefNum = refNum; |
pb.ioParam.ioBuffer = copyBuffer; |
pb.ioParam.ioReqCount = bufferSize; |
pb.ioParam.ioPosMode = fsFromStart; |
pb.ioParam.ioPosOffset = 0; |
err = PBReadAsync(&pb); |
// Did we complete early. Note that this sort of code often |
// needs to use an atomic test and set, but in this case that's |
// not necessary because MyCompletion doesn't ever read |
// gEarlyCompletion. |
*async = ! gEarlyCompletion; |
// Wait for the I/O to actually complete. |
while ( pb.ioParam.ioResult > 0 ) { |
// do nothing |
} |
// Clean up. |
DisposePtr( copyBuffer ); |
MoreAssertQ(MemError() == noErr); |
} |
return err; |
} |
static OSStatus IsDeviceAsync(FSVolumeRefNum vRefNum, Boolean *async, DriverRefNum *refNum, SInt16 *driveNum) |
// This routine asks the device driver whether it thinks it's asynchronous |
// using Driver Gestalt. |
{ |
OSStatus err; |
HParamBlockRec hpb; |
DriverGestaltParam pb; |
DriverFlags flags; |
// First find the driver and drive on which the volume is mounted. |
hpb.volumeParam.ioNamePtr = nil; |
hpb.volumeParam.ioVRefNum = vRefNum; |
hpb.volumeParam.ioVolIndex = 0; |
err = PBHGetVInfoSync(&hpb); |
// Special case for offline volumes. We're not going anywhere near them. |
if (err == noErr) { |
if (hpb.volumeParam.ioVDrvInfo == 0) { |
err = offLinErr; |
} |
} |
if (err == noErr) { |
*refNum = hpb.volumeParam.ioVDRefNum; |
*driveNum = hpb.volumeParam.ioVDrvInfo; |
} |
// Then check whether the driver support Driver Gestalt. |
if (err == noErr) { |
err = TradGetDriverInformation(*refNum, nil, &flags, nil, nil); |
} |
if (err == noErr) { |
if (TradDriverGestaltIsOn(flags)) { |
// If the driver supports Driver Gestalt, we ask it |
// whether it thinks it supports async. |
pb.ioVRefNum = *driveNum; |
pb.ioCRefNum = *refNum; |
pb.csCode = kDriverGestaltCode; |
pb.driverGestaltSelector = kdgSync; |
err = PBStatusSync( (ParamBlockRec *) &pb); |
if (err == noErr) { |
*async = ! (GetDriverGestaltSyncResponse(&pb)->behavesSynchronously); |
} |
} else { |
// If the driver doesn't support Driver Gestalt, |
// we return true. This will cause the caller |
// to try a real test based on I/O. We do this |
// because certain really old drivers, like ".Sony", |
// are async even though they don't support Driver |
// Gestalt. |
*async = true; |
} |
} |
return err; |
} |
#endif |
static OSStatus IsVolumeTrulyAsync(FSVolumeRefNum vRefNum, Boolean *async) |
// This routine sets *async to true if the volume can, within |
// the bounds of our ability to tell, operate asynchronously. |
// The algorithm is described above. |
{ |
OSStatus err; |
Boolean localVolume; |
UInt32 volumeBytesPerSecond; |
err = GetFileSystemInfo(vRefNum, async, &localVolume, &volumeBytesPerSecond); |
#if TARGET_API_MAC_CARBON |
// To a first approximation, all device drivers are async on Carbon. |
// Actually, we still have the Power Mac 6500 problem on Carbon on |
// traditional Mac OS, but I'm going to ignore that for the moment. |
#else |
{ |
DriverRefNum refNum; |
SInt16 driveNum; |
if (err == noErr && *async) { |
err = IsDeviceAsync(vRefNum, async, &refNum, &driveNum); |
} |
if (err == noErr && *async && localVolume) { |
err = IsDeviceTrulyAsync(async, volumeBytesPerSecond, refNum, driveNum); |
} |
} |
#endif |
return err; |
} |
///////////////////////////////////////////////////////////////// |
#pragma mark ----- Copy Engine ----- |
struct CopyParams { |
void *copyBuffer; |
ByteCount copyBufferSize; |
UInt32 recursionLimit; |
UInt32 currentDepth; |
Boolean copyingToDropFolder; |
Boolean copyingToLocalVolume; |
}; |
typedef struct CopyParams CopyParams; |
// The tasks stack size is a combination of two factors: |
// |
// 1. Non-recursive overhead, about 3 KB, which is the |
// sum of the frame sizes of the various non-recursive |
// routines in the maximum depth copy engine call chain. |
// |
// 2. Recursive overhead, about 200 bytes per recursion level, |
// which is the frame size of the CopyFolder routine. |
// |
// With the current numbers, this works out to less than 24 KB, |
// which is not a problem. |
enum { |
kCopyFolderFrameSize = 208, |
kCopyFileTaskStackSize = (3 * 1024) + (kMaximumFolderDepth * kCopyFolderFrameSize) |
}; |
static UTCDateTime gMagicBusyCreationDate; // a UTCDateTime equivalent of kMagicBusyCreationDate. |
static HFSUniStr255 gDataForkName; |
static HFSUniStr255 gRsrcForkName; |
static OSStatus CopyFork(const FSRef *source, SInt64 sourceForkSize, const FSRef *dest, |
SInt16 forkDestRefNum, |
ConstHFSUniStr255Param forkName, |
const CopyParams *params) |
// Copies the fork whose name is forkName from source to dest. |
// A refnum for the destination fork may be supplied in forkDestRefNum. |
// If forkDestRefNum is 0, we must open the destination fork ourselves, |
// otherwise it has been opened for us and we shouldn't close it. |
// |
// Frame Size: 128 |
{ |
OSStatus err; |
OSStatus err2; |
SInt16 sourceRef; |
SInt16 destRef; |
SInt64 bytesRemaining; |
SInt64 bytesToReadThisTime; |
SInt64 bytesToWriteThisTime; |
// If we haven't been passed in a forkDestRefNum (which basically |
// means we're copying into a non-drop folder), create the destination |
// fork. We have to do this regardless of whether sourceForkSize is |
// 0, because we want to preserve empty forks. |
if (forkDestRefNum == 0) { |
err = FSCreateFork(dest, forkName->length, forkName->unicode); |
// Mac OS 9.0 has a bug (in the AppleShare external file system, |
// I think) [2410374] that causes FSCreateFork to return an errFSForkExists |
// error even though the fork is empty. The following code swallows |
// the error (which is harmless) in that case. |
if (err == errFSForkExists && !params->copyingToLocalVolume) { |
err = noErr; |
} |
} |
// The remainder of this code only applies if there is actual data |
// in the source fork. |
if (err == noErr && sourceForkSize != 0) { |
// Prepare for failure. |
sourceRef = 0; |
destRef = 0; |
// Open up the destination fork, if we're asked to, otherwise |
// just use the passed in forkDestRefNum. |
if (forkDestRefNum == 0) { |
err = FSOpenFork(dest, forkName->length, forkName->unicode, fsWrPerm, &destRef); |
} else { |
destRef = forkDestRefNum; |
} |
// Open up the source fork. |
if (err == noErr) { |
err = FSOpenFork(source, forkName->length, forkName->unicode, fsRdPerm, &sourceRef); |
} |
// Here we create space for the entire fork on the destination volume. |
// FSAllocateFork has the right semantics on both traditional Mac OS |
// and Mac OS X. On traditional Mac OS it will allocate space for the |
// file in one hit without any other special action. On Mac OS X, |
// FSAllocateFork is preferable to FSSetForkSize because it prevents |
// the system from zero filling the bytes that were added to the end |
// of the fork (which would be waste becasue we're about to write over |
// those bytes anyway. |
if (err == noErr) { |
err = FSAllocateFork(destRef, kFSAllocNoRoundUpMask, fsFromStart, 0, sourceForkSize, nil); |
} |
// Copy the file from the source to the destination in chunks of |
// no more than params->copyBufferSize bytes. This is fairly |
// boring code except for the bytesToReadThisTime/bytesToWriteThisTime |
// distinction. On the last chunk, we round bytesToWriteThisTime |
// up to the next 512 byte boundary and then, after we exit the loop, |
// we set the file's EOF back to the real location (if the fork size |
// is not a multiple of 512 bytes). |
// |
// This technique works around a 'bug' in the traditional Mac OS File Manager, |
// where the File Manager will put the last 512-byte block of a large write into |
// the cache (even if we specifically request no caching) if that block is not |
// full. If the block goes into the cache it will eventually have to be |
// flushed, which causes sub-optimal disk performance. |
// |
// This is only done if the destination volume is local. For a network |
// volume, it's better to just write the last bytes directly. |
// |
// This is extreme over-optimisation given the other limits of this |
// sample, but I will hopefully get to the other limits eventually. |
bytesRemaining = sourceForkSize; |
while (err == noErr && bytesRemaining != 0) { |
if (bytesRemaining > params->copyBufferSize) { |
bytesToReadThisTime = params->copyBufferSize; |
bytesToWriteThisTime = bytesToReadThisTime; |
} else { |
bytesToReadThisTime = bytesRemaining; |
if (params->copyingToLocalVolume) { |
bytesToWriteThisTime = (bytesRemaining + 0x01FF) & ~0x01FF; |
} else { |
bytesToWriteThisTime = bytesRemaining; |
} |
} |
err = FSReadFork(sourceRef, fsAtMark + noCacheMask, 0, bytesToReadThisTime, params->copyBuffer, nil); |
if (err == noErr) { |
err = FSWriteFork(destRef, fsAtMark + noCacheMask, 0, bytesToWriteThisTime, params->copyBuffer, nil); |
} |
if (err == noErr) { |
bytesRemaining -= bytesToReadThisTime; |
} |
} |
if (err == noErr && (params->copyingToLocalVolume && ((sourceForkSize & 0x01FF) != 0)) ) { |
err = FSSetForkSize(destRef, fsFromStart, sourceForkSize); |
} |
// Clean up. |
if (sourceRef != 0) { |
err2 = FSCloseFork(sourceRef); |
MoreAssertQ(err2 == noErr); |
if (err == noErr) { |
err = err2; |
} |
} |
// Only close destRef if we were asked to open it (ie forkDestRefNum == 0) and |
// we actually managed to open it (ie destRef != 0). |
if (forkDestRefNum == 0 && destRef != 0) { |
err2 = FSCloseFork(destRef); |
MoreAssertQ(err2 == noErr); |
if (err == noErr) { |
err = err2; |
} |
} |
} |
return err; |
} |
// The ForkTracker data structure holds information about a specific fork, |
// specifically the name and the refnum. We use this to build a list of |
// all the forks before we start copying them. We need to do this because, |
// if we're copying into a drop folder, we must open all the forks before |
// we start copying data into any of them. |
// |
// For debug builds, we pad the ForkTracker record out to be a multiple of |
// 8 bytes long. This is because the debug builds use MPGetAllocatedBlockSize |
// in many assertions, but MPGetAllocatedBlockSize only returns the size of |
// the block to an 8 byte resolution. Rather than second guess its results, |
// we just ensure that the ForkTracker element size for debug build is |
// a multiple of 8. Also, we have to use 68K alignment because that's |
// the only guaranteed way to force a specific structure layout. |
#pragma options align=mac68k |
struct ForkTracker { |
HFSUniStr255 forkName; |
SInt64 forkSize; |
SInt16 forkDestRefNum; |
#if MORE_DEBUG |
SInt16 pad[3]; |
#endif |
}; |
typedef struct ForkTracker ForkTracker; |
typedef ForkTracker *ForkTrackerPtr; |
#pragma options align=reset |
static Boolean IdenticalHFSUniStr255(const HFSUniStr255 *lhs, const HFSUniStr255 *rhs) |
// MP Note: I can call memcmp here because it's implement in |
// a library that's statically linked in to our application and |
// I checked the code in that library and it doesn't break the |
// rules for pre-emptive tasks. |
// |
// Frame Size: 80 |
{ |
return (lhs->length == rhs->length) |
&& (memcmp(lhs->unicode, rhs->unicode, lhs->length * sizeof(UniChar)) == 0); |
} |
static OSStatus CalculateForksToCopy(const FSRef *source, |
SInt64 *dataForkSize, |
SInt64 *rsrcForkSize, |
ForkTrackerPtr *otherForksParam, |
ItemCount *otherForksCountParam) |
// This routine determines the list of forks that a file has. |
// dataFork is set if the file has a data fork. |
// rsrcFork is set if the file has a resource fork. |
// otherForksParam is set to point to a memory block allocated with |
// MPAllocAligned if the file has forks beyond the resource and data |
// forks. You must free that block with MPFree. otherForksCountParam |
// is set to the count of the number of forks in the otherForksParam |
// array. This count does *not* include the resource and data forks. |
// |
// Frame Size: 656 |
{ |
OSStatus err; |
Boolean done; |
CatPositionRec iterator; |
HFSUniStr255 thisForkName; |
SInt64 thisForkSize; |
ForkTrackerPtr otherForks; |
ItemCount otherForksCount; |
ItemCount otherForksMemoryBlockCount; |
#if MORE_DEBUG |
Boolean doneDataFork = false; |
Boolean doneRsrcFork = false; |
#endif |
MoreAssertQ(source != nil); |
MoreAssertQ(dataForkSize != nil); |
MoreAssertQ(rsrcForkSize != nil); |
MoreAssertQ(otherForksParam != nil); |
MoreAssertQ(*otherForksParam == nil); |
MoreAssertQ(otherForksCountParam != nil); |
MoreAssertQ(*otherForksCountParam == 0); |
MoreAssertQ(sizeof(ForkTracker) % 8 == 0); // See note above. |
*dataForkSize = 0; |
*rsrcForkSize = 0; |
otherForks = nil; |
otherForksCount = 0; |
// Iterate through the list of forks, processing each fork name in turn. |
iterator.initialize = 0; |
done = false; |
do { |
err = FSIterateForks(source, &iterator, &thisForkName, &thisForkSize, nil); |
if (err == errFSNoMoreItems) { |
err = noErr; |
done = true; |
} else if (err == noErr) { |
if ( kSpecialCaseClassicForks && IdenticalHFSUniStr255(&thisForkName, &gDataForkName) ) { |
#if MORE_DEBUG |
// only one data fork, please |
MoreAssertQ(doneDataFork == false); |
doneDataFork = true; |
#endif |
*dataForkSize = thisForkSize; |
} else if ( kSpecialCaseClassicForks && IdenticalHFSUniStr255(&thisForkName, &gRsrcForkName) ) { |
#if MORE_DEBUG |
// only one resource fork, please |
MoreAssertQ(doneRsrcFork == false); |
doneRsrcFork = true; |
#endif |
*rsrcForkSize = thisForkSize; |
} else { |
// We've found a fork other than the resource and data forks. |
// We have to add it to the otherForks array. But the array |
// a) may not have been created yet, and b) may not contain |
// enough elements to hold the new fork. |
if (otherForks == nil) { |
// The array hasn't been allocated yet, allocate it. |
otherForksMemoryBlockCount = kExpectedForkCount; |
otherForks = MPAllocateAligned(sizeof(ForkTracker) * kExpectedForkCount, kMPAllocateDefaultAligned, kNilOptions); |
if (otherForks == nil) { |
err = memFullErr; |
} |
} else { |
// If the array doesn't contain enough elements, grow it. |
if (otherForksCount == otherForksMemoryBlockCount) { |
ForkTrackerPtr newOtherForks; |
newOtherForks = MPAllocateAligned(sizeof(ForkTracker) * (otherForksCount + kExpectedForkCount), kMPAllocateDefaultAligned, kNilOptions); |
if (otherForks == nil) { |
err = memFullErr; |
} else { |
BlockMoveData(otherForks, newOtherForks, sizeof(ForkTracker) * otherForksCount); |
otherForksMemoryBlockCount += kExpectedForkCount; |
MPFree(otherForks); |
otherForks = newOtherForks; |
} |
} |
} |
// If we have no error, we know we have space in the otherForks |
// array to place the new fork. Put it there and increment the |
// count of forks. |
if (err == noErr) { |
MoreAssertQ(otherForks != nil); |
MoreAssertQ(otherForksCount < otherForksMemoryBlockCount); |
MoreAssertQ(MPGetAllocatedBlockSize(otherForks) == sizeof(ForkTracker) * otherForksMemoryBlockCount); |
BlockMoveData(&thisForkName, &otherForks[otherForksCount].forkName, sizeof(thisForkName)); |
otherForks[otherForksCount].forkSize = thisForkSize; |
otherForks[otherForksCount].forkDestRefNum = 0; |
otherForksCount += 1; |
} |
} |
} |
} while (err == noErr && ! done); |
// Clean up. |
if (err != noErr) { |
if (otherForks != nil) { |
MPFree(otherForks); |
otherForks = nil; |
} |
otherForksCount = 0; |
} |
*otherForksParam = otherForks; |
*otherForksCountParam = otherForksCount; |
// I had to break up the post-conditions into multiple statements |
// because otherwise they overflow the string length limit of the pre-processor. |
// Jeff Rohl would be proud. |
if (err == noErr) { |
// Success post-condition |
MoreAssertQ( ((otherForks == nil) && (otherForksCount == 0)) |
|| ((otherForks != nil) && (otherForksCount <= otherForksMemoryBlockCount) && (MPGetAllocatedBlockSize(otherForks) == sizeof(ForkTracker) * otherForksMemoryBlockCount))); |
} else { |
// Failure post-condition |
MoreAssertQ((otherForks == nil) && (otherForksCount == 0)); |
} |
return err; |
} |
static OSStatus OpenAllForks(const FSRef *dest, Boolean openDataFork, Boolean openRsrcFork, |
SInt16 *dataRefNum, SInt16 *rsrcRefNum, ForkTrackerPtr otherForks, ItemCount otherForksCount) |
// Open all the forks of the file. We need to do this when we're copying |
// into a drop folder, where you must open all the forks before starting |
// to write to any of them. |
// |
// IMPORTANT: If it fails, this routine won't close forks that opened successfully. |
// You must call CloseAllForks regardless of whether this routine returns an error. |
// |
// Frame Size: 96 |
{ |
OSStatus err; |
ItemCount thisForkIndex; |
MoreAssertQ(dataRefNum != nil); |
MoreAssertQ(rsrcRefNum != nil); |
MoreAssertQ(*dataRefNum == 0); |
MoreAssertQ(*rsrcRefNum == 0); |
MoreAssertQ((otherForks == nil) || (otherForksCount > 0)); |
// Open the resource and data forks as a special case. |
err = noErr; |
if (openDataFork) { |
// Data fork never needs to be created, so I don't have to FSCreateFork it here. |
err = FSOpenFork(dest, gDataForkName.length, gDataForkName.unicode, fsWrPerm, dataRefNum); |
} |
if (err == noErr && openRsrcFork) { |
// Resource fork never needs to be created, so I don't have to FSCreateFork it here. |
err = FSOpenFork(dest, gRsrcForkName.length, gRsrcForkName.unicode, fsWrPerm, rsrcRefNum); |
} |
// Open the other forks. |
if (err == noErr) { |
for (thisForkIndex = 0; thisForkIndex < otherForksCount; thisForkIndex++) { |
MoreAssertQ(otherForks[thisForkIndex].forkDestRefNum == 0); |
// Create the fork. Swallow afpAccessDenied because this operation |
// causes the external file system compatibility shim in Mac OS 9 to |
// generate a GetCatInfo request to the AppleShare external file system, |
// which in turn causes an AFP GetFileDirParms request on the wire, |
// which the AFP server bounces with afpAccessDenied because the file |
// is in a drop folder. As there's no native support for non-classic |
// forks in current AFP, there's no way I can decide how I should |
// handle this in a non-test case. So I just swallow the error and |
// hope that when native AFP support arrives, the right thing will happen. |
err = FSCreateFork(dest, otherForks[thisForkIndex].forkName.length, otherForks[thisForkIndex].forkName.unicode); |
if (err == noErr || err == afpAccessDenied) { |
err = noErr; |
} |
// Previously I avoided opening up the fork if the fork if the |
// length was empty, but that confused CopyFork into thinking |
// this wasn't a drop folder copy, so I decided to simply avoid |
// this trivial optimisation. In drop folders, we always open |
// all forks. |
if (err == noErr) { |
err = FSOpenFork(dest, otherForks[thisForkIndex].forkName.length, otherForks[thisForkIndex].forkName.unicode, fsWrPerm, &otherForks[thisForkIndex].forkDestRefNum); |
} |
if (err != noErr) { |
break; |
} |
} |
} |
return err; |
} |
static OSStatus CloseAllForks(SInt16 dataRefNum, SInt16 rsrcRefNum, ForkTrackerPtr otherForks, ItemCount otherForksCount) |
// Close all the forks that might have been opened by OpenAllForks. |
// |
// Frame Size: 96 |
{ |
OSStatus err; |
OSStatus err2; |
ItemCount thisForkIndex; |
MoreAssertQ((otherForks == nil) || (otherForksCount > 0)); |
err = noErr; |
if (dataRefNum != 0) { |
err2 = FSCloseFork(dataRefNum); |
MoreAssertQ(err2 == noErr); |
if (err == noErr) { |
err = err2; |
} |
} |
if (rsrcRefNum != 0) { |
err2 = FSCloseFork(rsrcRefNum); |
MoreAssertQ(err2 == noErr); |
if (err == noErr) { |
err = err2; |
} |
} |
for (thisForkIndex = 0; thisForkIndex < otherForksCount; thisForkIndex++) { |
if (otherForks[thisForkIndex].forkDestRefNum != 0) { |
err2 = FSCloseFork(otherForks[thisForkIndex].forkDestRefNum); |
MoreAssertQ(err2 == noErr); |
if (err == noErr) { |
err = err2; |
} |
} |
} |
return err; |
} |
static OSStatus CopyItemsForks(const FSRef *source, const FSRef *dest, CopyParams *params) |
// Copy all the folks of a file or folder from source to dest. |
// |
// Frame Size: 112 (+ 656 in sub-routines) |
{ |
OSStatus err; |
OSStatus err2; |
SInt64 dataForkSize; |
SInt64 rsrcForkSize; |
ForkTrackerPtr otherForks; |
ItemCount otherForksCount; |
SInt16 dataRefNum; |
SInt16 rsrcRefNum; |
ItemCount thisForkIndex; |
dataRefNum = 0; |
rsrcRefNum = 0; |
otherForks = nil; |
otherForksCount = 0; |
// First determine the list of forks that the source has. |
err = CalculateForksToCopy(source, &dataForkSize, &rsrcForkSize, &otherForks, &otherForksCount); |
if (err == noErr) { |
// If we're copying into a drop folder, open up all of those forks. |
// We have to do this because, once we've starting writing to a fork |
// in a drop folder, we can't open any more forks. |
// |
// We only do this if we're copying into a drop folder in order |
// to conserve FCBs in the more common, non-drop folder case. |
if (params->copyingToDropFolder) { |
err = OpenAllForks(dest, (dataForkSize != 0), (rsrcForkSize != 0), &dataRefNum, &rsrcRefNum, otherForks, otherForksCount); |
} |
// Copy each fork. |
if (err == noErr && (dataForkSize != 0)) { |
err = CopyFork(source, dataForkSize, dest, dataRefNum, &gDataForkName, params); |
} |
if (err == noErr && (rsrcForkSize != 0)) { |
err = CopyFork(source, rsrcForkSize, dest, rsrcRefNum, &gRsrcForkName, params); |
} |
if (err == noErr) { |
for (thisForkIndex = 0; thisForkIndex < otherForksCount; thisForkIndex++) { |
err = CopyFork(source, otherForks[thisForkIndex].forkSize, |
dest, otherForks[thisForkIndex].forkDestRefNum, &otherForks[thisForkIndex].forkName, params); |
if (err != noErr) { |
break; |
} |
} |
} |
// Close any forks that might be left open. Note that we have to call |
// this regardless of an error. Also note that this only closes forks |
// that were opened by OpenAllForks. If we're not copying into a drop |
// folder, the forks are opened and closed by CopyFork. |
err2 = CloseAllForks(dataRefNum, rsrcRefNum, otherForks, otherForksCount); |
if (err == noErr) { |
err = err2; |
} |
} |
// Clean up. |
if (otherForks != nil) { |
MPFree(otherForks); |
} |
return err; |
} |
static volatile UInt32 gState = 0; // Used only during debugging. |
static Boolean gFileSharingIsRunning; |
static OSStatus CopyFile(const FSRef *source, FSCatalogInfo *sourceCatInfo, |
const FSRef *destDir, ConstHFSUniStr255Param destName, |
CopyParams *params) |
// Copies a file referenced by source to the directory referenced by |
// destDir. destName is the name the file should be given in the |
// destination directory. sourceCatInfo is the catalogue info of |
// the file, which is passed in as an optimisation (we could get it |
// by doing a FSGetCatalogInfo but the caller has already done that |
// so we might as well take advantage of that). |
// |
// Frame Size: 176 (+ 112 + 656 in sub-routines) |
{ |
OSStatus err; |
OSStatus junk; |
FSRef dest; |
OSType originalFileType; |
UInt16 originalNodeFlags; |
UTCDateTime originalCreateDate; |
// If we're copying to a drop folder, we won't be able to reset this |
// information once the copy is done, so we don't mess it up in |
// the first place. We still clear the locked bit though; items dropped |
// into a drop folder always become unlocked. |
if (!params->copyingToDropFolder) { |
// Remember and clear the file's type, so the Finder doesn't |
// look at the file until we're done. |
originalFileType = ((FInfo *) &sourceCatInfo->finderInfo)->fdType; |
((FInfo *) &sourceCatInfo->finderInfo)->fdType = kFirstMagicBusyFiletype; |
// Set the file's creation date to kMagicBusyCreationDate, |
// remembering the old value for restoration later. |
originalCreateDate = sourceCatInfo->createDate; |
sourceCatInfo->createDate = gMagicBusyCreationDate; |
// Remember and clear the file's locked status, so that we can |
// actually write the forks we're about to create. |
originalNodeFlags = sourceCatInfo->nodeFlags; |
} |
sourceCatInfo->nodeFlags &= ~kFSNodeLockedMask; |
// Create the file in destDir. |
// In Mac OS 9.0, File Sharing messes up calls to FSCreateFileUnicode such that, |
// if you pass in sourceCatInfo, the file will be created with a blank file |
// type and creator code. [2397324] I work around this by, if File |
// Sharing is running, splitting the create into two parts: first create the file, |
// then set the catalogue info. This defeats the purpose of the unified call (ie |
// the operation isn't atomic) but them's the breaks. Also, I found that if you |
// set the catalogue info directly after creating the file, you trigger another |
// aspect of this File Sharing problem and you end up with weird zombie files that |
// are half locked and half unlocked. To solve this I don't set any catalogue |
// info here and set all the info at the end of the routine, where normally I just |
// restore the modification dates etc. |
// |
// This should be OK because the FSCreateFileUnicode creates a file with a zero |
// fdType, which the Finder will ignore. |
// |
// I'm scared now. |
if ( ! gFileSharingIsRunning || params->copyingToDropFolder) { |
// File sharing not running, let's do this the right way. |
err = FSCreateFileUnicode(destDir, destName->length, destName->unicode, kFSCatInfoSettableInfo, sourceCatInfo, &dest, nil); |
} else { |
// File sharing is running, use the workaround. |
err = FSCreateFileUnicode(destDir, destName->length, destName->unicode, kFSCatInfoNone, nil, &dest, nil); |
} |
// Iterate through the forks, copying each fork. |
if (err == noErr) { |
err = CopyItemsForks(source, &dest, params); |
// Restore the original file type, creation and modification dates, and |
// locked status. This is one of the places where we need to handle drop |
// folders as a special case because this FSSetCatalogInfo will fail for |
// an item in a drop folder, so we don't even attempt it. |
// |
// Also, if File Sharing is running we use this point to set up all |
// the catalogue info. |
if (err == noErr && !params->copyingToDropFolder) { |
((FInfo *) &sourceCatInfo->finderInfo)->fdType = originalFileType; |
sourceCatInfo->createDate = originalCreateDate; |
sourceCatInfo->nodeFlags = originalNodeFlags; |
if (gFileSharingIsRunning) { |
err = FSSetCatalogInfo(&dest, kFSCatInfoCreateDate |
| kFSCatInfoAttrMod |
| kFSCatInfoContentMod |
| kFSCatInfoFinderInfo |
| kFSCatInfoNodeFlags, sourceCatInfo); |
} else { |
err = FSSetCatalogInfo(&dest, kFSCatInfoSettableInfo, sourceCatInfo); |
} |
} |
// If we created the file and the copy failed, try to clean up by |
// deleting the file we created. We do this because, while it's |
// possible for the copy to fail halfway through and we don't really |
// clean up that well, we *really* don't wan't any half-created |
// files being left around. |
// |
// Note that there are cases where the assert can fire which are not |
// errors (for example, if the destination is in a drop folder) but |
// I'll leave it in anyway because I'm interested in discovering those |
// cases. Note that, if this fires and we're running MP, current versions |
// of MacsBug won't catch the exception and the MP task will terminate |
// with an kMPTaskAbortedErr error. |
if (err != noErr) { |
junk = FSDeleteObject(&dest); |
MoreAssertQ(junk == noErr); |
} |
} |
return err; |
} |
static OSStatus CopyFolder(const FSRef *source, FSCatalogInfo *sourceCatInfo, |
const FSRef *destDir, ConstHFSUniStr255Param destName, |
CopyParams *params) |
// Copies a folder referenced by source to the directory referenced by |
// destDir. destName is the name the folder should be given in the |
// destination directory. sourceCatInfo is the catalogue info of |
// the folder, which is passed in as an optimisation (we could get it |
// by doing a FSGetCatalogInfo but the caller has already done that |
// so we might as well take advantage of that). |
// |
// IMPORTANT: This routine calls itself recursively, so adding local |
// variables can significantly effect the stack usage of the operation. |
// If you add local variables, remember to update the constant |
// kCopyFolderFrameSize to be greater than or equal to the real frame size. |
// |
// Frame Size: 208 (see kCopyFolderFrameSize) (+ 176 + 112 + 656 in sub-routines) |
{ |
OSStatus err; // stack, 4 bytes |
OSStatus junk; // stack, 4 bytes |
FSRef newDir; // stack, 80 bytes |
FSIterator iterator; // stack, 4 bytes |
ItemCount foundItems; // stack, 4 bytes |
FSCatalogInfo *foundCatInfos; // heap, 144 bytes per element |
FSRef *foundFSRefs; // heap, 80 bytes per element |
HFSUniStr255 *foundNames; // heap, 512 bytes per element |
Boolean done; // stack, 4 bytes |
ItemCount i; // stack, 4 bytes |
UTCDateTime originalCreateDate; // stack, 8 bytes |
// Check that we haven't run off the bottom of our stack. |
err = noErr; |
params->currentDepth += 1; |
if (params->currentDepth > params->recursionLimit) { |
err = -1; |
} |
// Allocate memory for temporary buffers. |
// |
// 736 bytes per element per kFolderItemsPerBulkCall (currently 20) per folder nesting depth |
if (err == noErr) { |
foundCatInfos = MPAllocateAligned(kFolderItemsPerBulkCall * sizeof(FSCatalogInfo), |
kMPAllocateDefaultAligned, kNilOptions); |
foundFSRefs = MPAllocateAligned(kFolderItemsPerBulkCall * sizeof(FSRef), |
kMPAllocateDefaultAligned, kNilOptions); |
foundNames = MPAllocateAligned(kFolderItemsPerBulkCall * sizeof(HFSUniStr255), |
kMPAllocateDefaultAligned, kNilOptions); |
if (foundCatInfos == nil || foundFSRefs == nil || foundNames == nil) { |
err = memFullErr; |
} |
} |
// Create the destination directory. |
if (err == noErr) { |
// Set the folder's creation date to kMagicBusyCreationDate |
// so that the Finder doesn't mess with the folder while |
// it's copying. We remember the old value for restoration |
// later. We only do this if we're not copying to a drop |
// folder, because if we are copying to a drop folder we don't |
// have the opportunity to reset the information at the end of |
// this routine. |
if (!params->copyingToDropFolder) { |
originalCreateDate = sourceCatInfo->createDate; |
sourceCatInfo->createDate = gMagicBusyCreationDate; |
} |
err = FSCreateDirectoryUnicode(destDir, destName->length, destName->unicode, |
kFSCatInfoSettableInfo, |
sourceCatInfo, |
&newDir, nil, nil); |
} |
// With the new APIs, folders can have forks as well as files. Before |
// we start copying items in the folder, we |
if (err == noErr) { |
err = CopyItemsForks(source, &newDir, params); |
} |
// Iterate through the source code directory, copying each item to the newly |
// created directory. |
if (err == noErr) { |
err = FSOpenIterator(source, kFSIterateFlat, &iterator); |
if (err == noErr) { |
// Repeatedly call GetCatalogInfoBulk to get the contents |
// of the directory, and copy the contents. |
// |
// Note that we call GetCatalogInfoBulk with kFSCatInfoSettableInfo, |
// not kFSCatInfoGettableInfo, because there's no point getting |
// information that we can't set back when we're creating the copied item. |
done = false; |
do { |
err = FSGetCatalogInfoBulk(iterator, kFolderItemsPerBulkCall, |
&foundItems, nil, |
kFSCatInfoSettableInfo, |
foundCatInfos, foundFSRefs, nil, foundNames); |
if (err == errFSNoMoreItems) { |
// We ran off the end of the directory. Record that we're |
// done, but set err to noErr so that we process any partial |
// results. |
done = true; |
err = noErr; |
} |
if (err == noErr) { |
for (i = 0; i < foundItems; i++) { |
if (foundCatInfos[i].nodeFlags & kFSNodeIsDirectoryMask) { |
err = CopyFolder(&foundFSRefs[i], &foundCatInfos[i], &newDir, &foundNames[i], params); |
} else { |
err = CopyFile(&foundFSRefs[i], &foundCatInfos[i], &newDir, &foundNames[i], params); |
} |
if (err != noErr) { |
break; |
} |
} |
} |
} while (err == noErr && !done); |
junk = FSCloseIterator(iterator); |
MoreAssertQ(junk == noErr); |
} |
} |
// Reset the modification dates, except when copying to a drop folder |
// where this won't work. |
if (err == noErr && !params->copyingToDropFolder) { |
sourceCatInfo->createDate = originalCreateDate; |
err = FSSetCatalogInfo(&newDir, kFSCatInfoCreateDate |
| kFSCatInfoAttrMod |
| kFSCatInfoContentMod, sourceCatInfo); |
} |
// Clean up. |
if (foundCatInfos != nil) { |
MPFree(foundCatInfos); |
} |
if (foundFSRefs != nil) { |
MPFree(foundFSRefs); |
} |
if (foundNames != nil) { |
MPFree(foundNames); |
} |
params->currentDepth -= 1; |
return err; |
} |
enum { |
kDestInsideSourceErr = -1234 |
}; |
static OSStatus CheckForDestInsideSource(const FSRef *source, const FSRef *destDir) |
// Determines whether the destination directory is equal to the source |
// item, or whether it's nested inside the source item. Returns a |
// kDestInsideSourceErr if that's the case. We do this to prevent |
// endless recursion while copying. |
// |
// Frame Size: 288 |
{ |
OSStatus err; |
FSRef thisDir; |
Boolean done; |
FSCatalogInfo thisDirInfo; |
thisDir = *destDir; |
done = false; |
do { |
err = FSCompareFSRefs(source, &thisDir); |
if (err == noErr) { |
err = kDestInsideSourceErr; |
} else if (err == diffVolErr) { |
err = noErr; |
done = true; |
} else if (err == errFSRefsDifferent) { |
// This is somewhat tricky. We can ask for the parent of thisDir |
// by setting the parentRef parameter to FSGetCatalogInfo but, if |
// thisDir is the volume's FSRef, this will give us back junk. |
// So we also ask for the parent's dir ID to be returned in the |
// FSCatalogInfo record, and then check that against the node |
// ID of the root's parent (ie 1). If we match that, we've made |
// it to the top of the hierarchy without hitting source, so |
// we leave with no error. |
err = FSGetCatalogInfo(&thisDir, kFSCatInfoParentDirID, &thisDirInfo, nil, nil, &thisDir); |
if (err == noErr) { |
if (thisDirInfo.parentDirID == fsRtParID) { |
done = true; |
} |
} |
} |
} while ( err == noErr && ! done ); |
return err; |
} |
enum { |
kPrivilegesMask = kioACUserNoSeeFolderMask | kioACUserNoSeeFilesMask | kioACUserNoMakeChangesMask, |
kDropFolderValue = kioACUserNoSeeFolderMask | kioACUserNoSeeFilesMask |
}; |
static OSStatus CopyItemTopLevel(const FSRef *source, const FSRef *destDir, |
void *copyBuffer, ByteCount copyBufferSize) |
// This routine acts as the top level of the copy engine. It exists |
// to a) present a nicer API than the various recursive routines, and |
// b) minimise the local variables in the recursive routines. |
// |
// Frame Size: 752 |
{ |
OSStatus err; |
FSCatalogInfo tmpCatInfo; |
HFSUniStr255 destName; |
CopyParams params; |
FSVolumeRefNum destVRefNum; |
params.copyBuffer = copyBuffer; |
params.copyBufferSize = copyBufferSize; |
params.recursionLimit = kMaximumFolderDepth; |
params.currentDepth = 0; |
// Get info on the destination to determine whether it is a drop folder. |
err = FSGetCatalogInfo(destDir, kFSCatInfoUserPrivs, &tmpCatInfo, nil, nil, nil); |
if (err == noErr) { |
params.copyingToDropFolder = ((tmpCatInfo.userPrivileges & kPrivilegesMask) == kDropFolderValue); |
// Get info on the source to decide whether to kick off a file or |
// directory copy. |
err = FSGetCatalogInfo(source, kFSCatInfoSettableInfo, &tmpCatInfo, &destName, nil, nil); |
} |
// Get info on the destination to determine whether it is on a network volume. |
if (err == noErr) { |
err = VolumeContainingFSRef(destDir, &destVRefNum); |
} |
if (err == noErr) { |
err = GetFileSystemInfo(destVRefNum, nil, ¶ms.copyingToLocalVolume, nil); |
} |
if (err == noErr) { |
// Clear the "inited" bit so that the Finder positions the icon for us. |
// We do this here because we only want to clear the "inited" bit for the |
// top level item. |
((FInfo *)(tmpCatInfo.finderInfo))->fdFlags &= ~kHasBeenInited; |
if (tmpCatInfo.nodeFlags & kFSNodeIsDirectoryMask) { |
err = CheckForDestInsideSource(source, destDir); |
if (err == noErr) { |
err = CopyFolder(source, &tmpCatInfo, destDir, &destName, ¶ms); |
} |
} else { |
err = CopyFile(source, &tmpCatInfo, destDir, &destName, ¶ms); |
} |
} |
return err; |
} |
///////////////////////////////////////////////////////////////// |
#pragma mark ----- MP Stuff ----- |
static Boolean MPFileManagerAvailable(void) |
// Check to see whether we can use the File Manager from a |
// pre-emptive thread. If gestaltMPCallableAPIsAttr isn't |
// available in your "Gestalt.h", update to the very latest. |
{ |
SInt32 response; |
return MPLibraryIsLoaded() |
&& Gestalt(gestaltMPCallableAPIsAttr, &response) == noErr |
&& (response & (1 << gestaltMPFileManager)) != 0; |
} |
// MP tasks take a single parameter when they start, but we need |
// to pass a bunch of parameters to the task, so we put them |
// in a structure. |
struct CopyTaskParams { |
const FSRef *source; |
const FSRef *destDir; |
void *copyBuffer; |
ByteCount copyBufferSize; |
}; |
typedef struct CopyTaskParams CopyTaskParams; |
static OSStatus CopyFileTask(const CopyTaskParams *params) |
// This is the start routine of the MP task. It simply |
// unpacks the parameters from the CopyTaskParams structure |
// and calls the copy engine. |
// |
// Frame Size: 64 |
{ |
OSStatus err; |
err = CopyItemTopLevel(params->source, params->destDir, params->copyBuffer, params->copyBufferSize); |
return err; |
} |
static OSStatus CopyItemTopLevelMP(const FSRef *source, const FSRef *destDir, |
void *copyBuffer, ByteCount copyBufferSize) |
// This routine is a direct analogue of CopyFileTopLevel, except it |
// executes the copy in an MP task. The foreground process than |
// simply sits and waits for the task to complete, printing "."s |
// every now and again. This is not a particularly useful application |
// of multi-processing (-: but it's still pretty cool. For a start, |
// you can hold down the mouse button on a menu and the copy keeps |
// going. |
// |
// In a real application, this routine would probably be executed |
// by co-operative code that would drive the status window that |
// shows the user the status of the copy. Pressing the Cancel |
// button on the status window would set a variable in the |
// CopyTaskParams that the MP task would periodically check. |
// [Calling MPTerminateTask would probably be a bad idea because |
// there'd be no way for the task to dispose of the memory that |
// it's allocated during the copy.] |
{ |
OSStatus err; |
OSStatus junk; |
MPQueueID terminationQueue; |
CopyTaskParams params; |
MPTaskID theCopyTask; |
Boolean done; |
OSStatus terminationErr; |
UInt32 timeLastPrinted; |
EventRecord event; |
terminationErr = noErr; // Just to make for more obvious debugging. |
// Fill out the CopyTaskParams. |
params.source = source; |
params.destDir = destDir; |
params.copyBuffer = copyBuffer; |
params.copyBufferSize = copyBufferSize; |
// Create the termination queue and the task itself. |
err = MPCreateQueue(&terminationQueue); |
if (err == noErr) { |
err = MPCreateTask( (TaskProc) CopyFileTask, ¶ms, |
kCopyFileTaskStackSize, |
terminationQueue, |
nil, nil, // no termination parameters |
0, // no task options |
&theCopyTask); |
// Now wait for the copying task to complete, printing "."s |
// while we wait. |
if (err == noErr) { |
done = false; |
timeLastPrinted = TickCount(); |
do { |
err = MPWaitOnQueue(terminationQueue, |
nil, nil, // not interested in these terminator parameters |
(void **) &terminationErr, |
kDurationImmediate); |
if (err == noErr) { |
done = true; |
err = terminationErr; |
} else if (err == kMPTimeoutErr) { |
if (TickCount() > timeLastPrinted + 10) { |
printf("."); |
fflush(stdout); |
timeLastPrinted = TickCount(); |
} |
(void) WaitNextEvent(everyEvent, &event, 10, nil); |
(void) SIOUXHandleOneEvent(&event); |
err = noErr; |
} |
} while (err == noErr && ! done ); |
} |
junk = MPDeleteQueue(terminationQueue); |
MoreAssertQ(junk == noErr); |
} |
return err; |
} |
///////////////////////////////////////////////////////////////// |
#pragma mark ----- Mainline ----- |
static OSStatus ExtractSingleItem(const NavReplyRecord *reply, FSRef *item) |
// This item extracts a single FSRef from a NavReplyRecord. |
// Nav makes it really easy to support 'odoc', but a real pain |
// to support other things. *sigh* |
{ |
OSStatus err; |
SInt32 itemCount; |
FSSpec fss; |
AEKeyword junkKeyword; |
DescType junkType; |
Size junkSize; |
MoreAssertQ((AECountItems(&reply->selection, &itemCount) == noErr) && (itemCount == 1)); |
err = AEGetNthPtr(&reply->selection, 1, typeFSS, &junkKeyword, &junkType, &fss, sizeof(fss), &junkSize); |
if (err == noErr) { |
MoreAssertQ(junkType == typeFSS); |
MoreAssertQ(junkSize == sizeof(FSSpec)); |
// We call FSMakeFSSpec because sometimes Nav is braindead |
// and gives us an invalid FSSpec (where the name is empty). |
// While FSpMakeFSRef seems to handle that (and the file system |
// engineers assure me that that will keep working (at least |
// on traditional Mac OS) because of the potential for breaking |
// existing applications), I'm still wary of doing this so |
// I regularise the FSSpec by feeding it through FSMakeFSSpec. |
if (fss.name[0] == 0) { |
err = FSMakeFSSpec(fss.vRefNum, fss.parID, fss.name, &fss); |
} |
if (err == noErr) { |
err = FSpMakeFSRef(&fss, item); |
} |
} |
return err; |
} |
static OSStatus ChooseSourceAndDest(FSRef *source, FSRef *dest) |
// Use Navigation Services (because I'm a good Apple employee) |
// to choose a source file and a destination folder, |
// and return them as FSRefs. |
{ |
OSStatus err; |
OSStatus junk; |
NavDialogOptions options; |
NavReplyRecord reply; |
err = noErr; |
if (! NavServicesAvailable() ) { |
printf("Navigation Services is required.\n"); |
err = -1; |
} |
if (err == noErr) { |
err = NavGetDefaultDialogOptions(&options); |
} |
// Get the source item. |
if (err == noErr) { |
options.dialogOptionFlags &= ~kNavAllowMultipleFiles; |
err = NavChooseObject(nil, &reply, &options, nil, nil, nil); |
} |
if (err == noErr) { |
err = ExtractSingleItem(&reply, source); |
junk = NavDisposeReply(&reply); |
MoreAssertQ(junk == noErr); |
} |
// Get the dest folder. |
if (err == noErr) { |
err = NavChooseFolder(nil, &reply, &options, nil, nil, nil); |
} |
if (err == noErr) { |
err = ExtractSingleItem(&reply, dest); |
junk = NavDisposeReply(&reply); |
MoreAssertQ(junk == noErr); |
} |
// Would be nice if I made the destination name unique at this point. |
// Too hard for the moment. |
return err; |
} |
void main(void) |
// The main line. Calls the copy engine to copy an item |
// named "Source" in the same directory as the application |
// to a folder named "Dest" in the same directory. |
{ |
OSStatus err; |
FSRef source; |
FSVolumeRefNum sourceVol; |
FSRef dest; |
FSVolumeRefNum destVol; |
Ptr copyBuffer; |
Boolean runPreemptive; |
ByteCount bufferSize; |
printf("Hello Cruel World!\n"); |
printf("MPFileCopy.c\n"); |
// gState is just for debugging. |
printf("&gState = $%08x\n", &gState); |
// We flush stdout so that the SIOUX window comes up, which initialises |
// the toolbox for us, which we're going to need as soon as we |
// call ChooseSourceAndDest, which calls Navigation Services. |
fflush(stdout); |
#if !TARGET_API_MAC_CARBON |
gMyCompletionUPP = NewIOCompletionProc(MyCompletion); |
MoreAssert(gMyCompletionUPP != nil); |
#endif |
copyBuffer = nil; |
// Get source and dest objects using Nav. |
err = ChooseSourceAndDest(&source, &dest); |
// Decide whether we can run using MP tasks or not. |
if (err == noErr) { |
err = VolumeContainingFSRef(&source, &sourceVol); |
} |
if (err == noErr) { |
err = VolumeContainingFSRef(&dest, &destVol); |
} |
runPreemptive = ! kNeverUseMPTasks && MPFileManagerAvailable(); |
if (err == noErr && runPreemptive) { |
err = IsVolumeTrulyAsync(sourceVol, &runPreemptive); |
if (err == noErr && runPreemptive && destVol != sourceVol) { |
err = IsVolumeTrulyAsync(destVol, &runPreemptive); |
} |
} |
// Allocate the copy buffer. We do this late because we |
// need to know the source and destination volumes to |
// judge the copy buffer size. |
if (err == noErr) { |
bufferSize = BufferSizeForThisVolume(sourceVol); |
if (destVol != sourceVol) { |
ByteCount tmp; |
tmp = BufferSizeForThisVolume(destVol); |
if (tmp < bufferSize) { |
bufferSize = tmp; |
} |
} |
copyBuffer = NewPtr(bufferSize); |
err = MemError(); |
} |
// Under Mac OS 9.0, File Sharing's patches cause FSCreateFileUnicode to go |
// astray. We determine whether it's running now and then test the gFileSharingIsRunning |
// flag later to see whether we need to use the workaround. |
if (err == noErr) { |
err = IsFileSharingRunning(&gFileSharingIsRunning); |
} |
// The copy engine is going to set each item's creation date |
// to kMagicBusyCreationDate while it's copying the item. |
// But kMagicBusyCreationDate is an old-style 32-bit date/time, |
// while the HFS Plus APIs use the new 64-bit date/time. So |
// we have to call a happy UTC utilities routine to convert from |
// the local time kMagicBusyCreationDate to a UTCDateTime |
// gMagicBusyCreationDate, which the File Manager will store |
// on disk and which the Finder we read back using the old |
// APIs, whereupon the File Manager will convert it back |
// to local time (and hopefully get the kMagicBusyCreationDate |
// back!). |
if (err == noErr) { |
gMagicBusyCreationDate.highSeconds = 0; |
gMagicBusyCreationDate.fraction = 0; |
err = ConvertLocalTimeToUTC(kMagicBusyCreationDate, &gMagicBusyCreationDate.lowSeconds); |
} |
// Get the constant names for the resource and data fork, which |
// we're going to need inside the copy engine. |
if (err == noErr) { |
err = FSGetDataForkName(&gDataForkName); |
} |
if (err == noErr) { |
err = FSGetResourceForkName(&gRsrcForkName); |
} |
// Now call copy engine. |
if (err == noErr) { |
if ( runPreemptive ) { |
printf("Copying using pre-emptive thread.\n"); |
fflush(stdout); |
err = CopyItemTopLevelMP(&source, &dest, copyBuffer, GetPtrSize(copyBuffer)); |
} else { |
printf("Copying using co-operative main thread.\n"); |
fflush(stdout); |
err = CopyItemTopLevel(&source, &dest, copyBuffer, GetPtrSize(copyBuffer)); |
} |
} |
// Clean up. |
if (copyBuffer != nil) { |
DisposePtr(copyBuffer); |
MoreAssertQ(MemError() == noErr); |
} |
if (err == noErr) { |
printf("\nSuccess.\n"); |
} else { |
printf("\nFailed with error %ld.\n", err); |
} |
printf("Done. Press command-Q to Quit.\n"); |
} |
Copyright © 2003 Apple Computer, Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2003-01-14