Skip to content
WindowsDeep Dive Published Updated 8 min readViews unavailable

Windows CopyFile2: Progress, Cancellation, and Safe Publication

Use CopyFile2 callbacks for progress and cancellation, handle partial destinations and metadata limits, and publish verified copies without claiming atomicity.

Copying a large file in a desktop application sounds like a single filesystem operation, but a useful user experience needs progress, cancellation, and a safe handoff to the rest of the application. Win32’s CopyFile2 performs the copy while reporting typed progress messages to a callback. The callback can continue, stop, pause, or cancel the operation. The API is still synchronous: the thread that calls CopyFile2 remains inside the copy until it completes, pauses, or stops.

That distinction drives the design. Run the call on a worker rather than blocking a window or service control thread. Treat callback messages as typed records, not as one universal progress structure. Interpret the final HRESULT, and keep the destination out of the live namespace until validation is complete. A successful copy is not the same as an atomic deployment, a content-integrity check, or proof that security descriptors were duplicated.

Set the deployment and destination contract first

CopyFile2 is supported starting with Windows 8 and Windows Server 2012; applications compile against the Windows SDK declaration with a Windows 8-or-later target. It copies one existing file to one new destination file and exposes stream and chunk callbacks. If the supported client includes an older Windows release, choose and test a compatible alternative instead of assuming this API is a runtime-resolved convenience function.

Use a unique staging path on the same volume as the final file when the later promotion workflow depends on a same-volume rename. Set COPY_FILE_FAIL_IF_EXISTS so a collision is a visible error rather than an accidental overwrite. Never let consumers open a partially copied staging file. After CopyFile2 succeeds, verify expected size, hash, file type, alternate streams if they are part of the contract, and any required ACL policy. Only then promote the staged artifact through a separately reviewed publication step.

The function preserves extended attributes, OLE structured storage, NTFS alternate data streams, and file attributes according to its documentation. Microsoft explicitly notes that security attributes for the source are not copied. Do not infer that the destination has the same DACL, owner, integrity label, or inherited permissions. Decide whether the staging directory’s inherited ACL is correct, apply the intended security policy, and inspect the result under the identity that will consume it.

Interpret callback messages through their discriminant

CopyFile2ProgressRoutine receives a COPYFILE2_MESSAGE and callback context. The Type member determines which member of the Info union is valid. A chunk-finished message carries cumulative byte counts; a stream-started message indicates that source and destination handles have been opened, but Microsoft specifically says it does not mean the stream’s data copy has started. A poll-continue message contains no byte counters. An error message has a failure code and copy phase. Read only the fields associated with the active type.

Progress is an observation of the API operation, not a durable checkpoint for resuming arbitrary work. A file may contain multiple streams. Report aggregate file bytes from the total-file fields when available, and do not add each stream’s counters together as though they were disjoint copies of the same total. File size can also change between the time a UI computes a percentage and the time a message arrives; present progress as approximate and keep a determinate indicator only when the denominator is known.

The following example records the latest cumulative total and checks a cancellation request from application-owned state. The callback context remains alive until the synchronous CopyFile2 call returns:

#define _WIN32_WINNT 0x0602
#include <windows.h>
#include <atomic>

struct CopyState {
    std::atomic<bool> cancelRequested{false};
    std::atomic<ULONGLONG> totalBytesTransferred{0};
};

COPYFILE2_MESSAGE_ACTION CALLBACK CopyProgress(
    const COPYFILE2_MESSAGE* message, void* context) noexcept {
    auto* state = static_cast<CopyState*>(context);
    if (message->Type == COPYFILE2_CALLBACK_CHUNK_FINISHED) {
        state->totalBytesTransferred.store(
            message->Info.ChunkFinished.uliTotalBytesTransferred.QuadPart,
            std::memory_order_relaxed);
    }
    if (state->cancelRequested.load(std::memory_order_relaxed)) {
        return COPYFILE2_PROGRESS_CANCEL;
    }
    return COPYFILE2_PROGRESS_CONTINUE;
}

HRESULT CopyToStaging(const wchar_t* source,
                      const wchar_t* uniqueStagingPath,
                      CopyState& state) {
    COPYFILE2_EXTENDED_PARAMETERS parameters{};
    parameters.dwSize = sizeof(parameters);
    parameters.dwCopyFlags = COPY_FILE_FAIL_IF_EXISTS;
    parameters.pfCancel = nullptr;
    parameters.pProgressRoutine = CopyProgress;
    parameters.pvCallbackContext = &state;
    return CopyFile2(source, uniqueStagingPath, &parameters);
}

This is a synchronous call wrapped in a helper, not a thread-pool implementation. Invoke it on a worker and marshal progress to the UI using a bounded, thread-safe channel. Do not update UI controls, pump a nested message loop, or wait on another worker from the callback. Keep the context alive for the entire call and make the cancellation flag’s lifetime independent of the window that initiated the operation.

Cancellation, pause, and stop are distinct outcomes

Returning COPYFILE2_PROGRESS_CANCEL requests cancellation. The documented result is HRESULT_FROM_WIN32(ERROR_REQUEST_ABORTED), and the API deletes partially copied fragments. The application must still inspect the returned HRESULT; the user’s click is only a request, and the copy can reach completion before a callback observes it. If the result is S_OK, treat the copy as completed and validate it rather than telling the user it was cancelled.

COPYFILE2_PROGRESS_STOP also returns the aborted result, but it leaves partial fragments intact. Microsoft documents resuming such a copy only when COPY_FILE_RESTARTABLE was set in the extended parameters, then using the resume-from-pause flag when the operation is restarted. COPYFILE2_PROGRESS_PAUSE has its own paused result and writes a resume header; if the copy is already complete as the pause is processed, it can instead complete successfully without that header. These states are not interchangeable cleanup choices. Unless the product has a tested resume design, use cancel and delete the staging path rather than preserving undocumented fragments.

There are two safe places to test a cancellation path: the callback can return an action when it receives a progress message, and the extended-parameters structure can carry the documented cancellation flag. Regardless of mechanism, cancellation is cooperative with the copy’s progress path; it is not a hard real-time interrupt. Once the function returns, clean up only the exact private staging name owned by that request, and keep the final published file untouched.

Make progress useful without making it authoritative

Handle message types explicitly and avoid reading a union field unless the corresponding Type matches. A progress handler should be quick and non-throwing. Use atomics for simple counters or enqueue compact immutable updates onto a bounded queue; if the UI cannot keep up, coalesce progress reports instead of accumulating one event per copied chunk. Preserve the callback’s error message details for diagnostics, but do not treat an error callback as the final result in place of CopyFile2’s return value.

The API returns an HRESULT. Use SUCCEEDED/FAILED or exact comparisons for expected values such as S_OK, ERROR_REQUEST_PAUSED, and ERROR_REQUEST_ABORTED. Record both the HRESULT and the source/destination operation identity. A destination collision under COPY_FILE_FAIL_IF_EXISTS can be reported as ERROR_ALREADY_EXISTS or ERROR_FILE_EXISTS; other filesystem, sharing, access, or network conditions can produce different failures. Do not feed a Win32 GetLastError() value into the report as though it were the primary return code.

If the source is on a network share, test the actual SMB version, failover behavior, credentials, and interruption model. CopyFile2 supports documented Windows 8-era SMB 3 and clustered storage cases, while newer flags have narrower version and medium requirements. In particular, compressed-traffic requests can be ignored where unsupported; the function does not expose compression complexity or memory tuning. A successful request for an optimization is not proof that the transport used it.

Validate the published artifact, not only the copy call

Use a unique destination in an access-controlled staging directory. On success, verify the content against an expected digest or trusted manifest, inspect required alternate streams and attributes, apply the destination security descriptor explicitly, and confirm the consuming identity can read the result. A content hash proves equality with the expected bytes only when the expected hash came from a trusted source; it does not authenticate the publisher by itself.

Make the final replacement a separate operation with a documented collision and rollback policy. Keep a prior known-good file until health checks pass, and do not allow an interrupted transfer to replace the live name. If a business operation requires a coherent snapshot while another process modifies the source, coordinate with the writer or use an appropriate snapshot mechanism before copying. S_OK reports the copy operation’s success, not application-level consistency, authorization, or provenance.

Test on the minimum supported Windows version and on every relevant storage class. Cover zero-byte and large files, multiple streams, hidden/read-only existing destinations, a name collision, callback cancellation, cancellation late in the operation, pause/stop only if implemented, low disk space, access denial, network interruption, hash mismatch, destination ACL inspection, and cleanup after process failure. A cross-compiler can check declarations and syntax; it cannot reproduce Windows filesystem callbacks, sharing behavior, or SMB failure timing.

CopyFile2 is valuable when the product needs a managed copy operation with progress and a typed cancellation boundary. Its strongest use is as one stage in a larger transaction: copy into a private name, validate content and permissions, and publish only after the application has enough evidence to trust the result.

Related:

Sources:

Comments