0

The code below is a snippet from my project that among other things:

  1. Gets a collection of folder contents from a specified local folder (The Upload folder)
  2. Copies the contained files to a remote network folder. If there are sub folders their files are copied, but the folder structure is not preserved.
  3. Moves the original local folders to a different local location (The Archive folder)

The trouble I'm having is in the move operation. If the folder to be moved contains one or more sub folders, the move fails with an 'access denied' error. I've narrowed the cause down to a previous function call that generates the list of files for copying. I've highlighted this call in the code below. It's in main (see first call to getFolderItemsRecursive()). I don't understand why this recursive function is creating issues. Its as if by collecting the information its in someway marking something as 'in use'. Note that I'm not actually copying the files as I commented these lines out already. So that's not it.

To run the code below you'll need to create a couple of folders somewhere and enter the locations into the code. The code assumes c:_UploadTests and c:_archivedtests. To reproduce the issue create at least one subfolder, with its own subfolder, in _UploadTest. E.g. c:_UploadTests\Foo\Bar\

#include "windows.h"
#include "stdafx.h"
#include <cstdint>
#include <vector>

#define RECURSION_DEPTH_NON 0
#define RECURSION_DEPTH_FULL -1

// Object needed bacause WIN32_FIND_DATAA doesn't contain file path data and we need this with each object.
typedef struct fileObject{
    std::string path;
    WIN32_FIND_DATAA metaData;
} fileObject;

void getFolderItems(std::vector<WIN32_FIND_DATAA>& output, const std::string& path) {
    WIN32_FIND_DATAA findfiledata;
    HANDLE hFind = INVALID_HANDLE_VALUE;

    char fullpath[MAX_PATH];
    GetFullPathNameA(path.c_str(), MAX_PATH, fullpath, 0);
    std::string fp(fullpath);

    hFind = FindFirstFileA((fp + "\\*").c_str(), &findfiledata);
    if (hFind != INVALID_HANDLE_VALUE) {
        do {
            switch (findfiledata.dwFileAttributes) {
            case FILE_ATTRIBUTE_DIRECTORY:                  // Its a directory
                if (findfiledata.cFileName[0] != '.')
                    output.push_back(findfiledata);
                break;
            case FILE_ATTRIBUTE_NORMAL:                     // Its a file
            case FILE_ATTRIBUTE_ARCHIVE:
            case FILE_ATTRIBUTE_COMPRESSED:
                output.push_back(findfiledata);
                break;
            }
        } while (FindNextFileA(hFind, &findfiledata) != 0);
    }
}

/// Gets a list of directory contents and their sub folders under a specified path
/// @param[out] output Empty vector to be filled with result
/// @param[in]  path   Input path, may be a relative path from working dir
/// @param[in]  depth  Max depth of recursion relative to 'path'. -1 = full recursion, 0 = non, 1 = allow one level down, etc
void getFolderItemsRecursive(std::vector<fileObject>& output, const std::string& path, int depth) {
    std::vector<WIN32_FIND_DATAA> thisLvl;
    fileObject fileObj;

    // Get all the items from this level folder
    getFolderItems(thisLvl, path);

    // Loop through the items and dive deeper into any subfolders
    for (std::vector<WIN32_FIND_DATAA>::iterator i = thisLvl.begin(); i != thisLvl.end(); ++i) {
        // Add the current folder to the object list
        fileObj.metaData = *i;
        fileObj.path = path;
        output.push_back(fileObj);

        if (fileObj.metaData.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY) {
            if (depth == RECURSION_DEPTH_FULL)
                getFolderItemsRecursive(output, path + std::string("\\") + fileObj.metaData.cFileName, RECURSION_DEPTH_FULL);
            else if (depth > 0)
                getFolderItemsRecursive(output, path + std::string("\\") + fileObj.metaData.cFileName, depth--);
        }
    }
}


int main(int argc, char** argv) {
    // manually create the list of upload folders
    std::vector<std::string> uploadFoldersVector;
    uploadFoldersVector.push_back("c:\\_UploadTests");

    // For each upload folder, get a list of sub folders and loop through them copying out the files.
    for (uint8_t i = 0; i < uploadFoldersVector.size(); i++) {
        std::string srcPath;
        std::string dstPath;
        BYTE flags;

        // Get a list of this folders contents
        std::vector<fileObject> uploadFiles;

        /*********************
        The function below seems to be the culprit when called with RECURSION_DEPTH_FULL - look in all subfolders and their children.
        A similar call but with RECURSION_DEPTH_NON doesn't cause any issues.
        **********************/
        getFolderItemsRecursive(uploadFiles, uploadFoldersVector[0], RECURSION_DEPTH_FULL);

        // For each file object, copy it to the network archive.
            // Removed for StackOverflow for simplicity - adds nothing to the problem definition

        // Finally move this folder into the local archive folder
            // Get a list of immediate sub folders
            uploadFiles.clear();
            getFolderItemsRecursive(uploadFiles, uploadFoldersVector[0], RECURSION_DEPTH_NON);

            flags = MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING;

            for (uint8_t j = 0; j < uploadFiles.size(); j++) {
                srcPath = uploadFoldersVector[0] + "\\" + uploadFiles[j].metaData.cFileName;
                dstPath = "c:\\_archivedtests" + (std::string)uploadFiles[j].metaData.cFileName;

                if (!MoveFileExA(srcPath.c_str(), dstPath.c_str(), flags)) {
                    fprintf(stderr, "Error moving folder %s to local archive. \n\tWindows returned code: %ld\n", srcPath.c_str(), GetLastError());
                }
            }
    }

    getchar();
    return 0;
}
Asesh
  • 3,186
  • 2
  • 21
  • 31
Ben
  • 427
  • 5
  • 17
  • `GetLastError()` not enough informative in this case. need call `RtlGetLastNtStatus()` here. `MoveFile` can return access denied by several reasons - `STATUS_FILE_IS_A_DIRECTORY` - when you try move folder to different volume or `STATUS_ACCESS_DENIED` - *A directory cannot be renamed if it has any open handles or if it or any of its subdirectories contains a file that has open handles*. and for what you build list of files ? why you not just copy/move in loop ? the move (in sense rename) need only top folders – RbMm Oct 06 '18 at 07:04
  • and your code can potential work only in case you move to the same volume. and in this case you need move only top folders, not files or subfolders. in case different volume you need yourself create folders, move files, delete folders – RbMm Oct 06 '18 at 07:10

1 Answers1

0

getFolderItems() is not calling FindClose() after the iteration loop is finished, so you are leaking open handles to folders that are being iterated, that is why you see them as "in use".

if (hFind != INVALID_HANDLE_VALUE) {
    do {
        ...
    } while (FindNextFileA(hFind, &findfiledata) != 0);
    FindClose(hFind); // <-- ADD THIS!
}

getFolderItems() also has other bugs, too:

  • if (findfiledata.cFileName[0] != '.') is the wrong check to use, as there can be folders present other than . and .. that also begin with a ., so you need to use this kind of check instead:

    if ((strcmp(findfiledata.cFileName, ".") != 0) && (strcmp(findfiledata.cFileName, "..") != 0))
    
  • using a switch to check for attributes is wrong, as entries may have multiple attributes present. You need to use the bitwise AND (&) operator to check for specific attributes:

    if (findfiledata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
    {
        // Its a directory
        ...
    }
    else
    {
        // Its a file
        if (findfiledata.dwFileAttributes & (FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_ARCHIVE | FILE_ATTRIBUTE_COMPRESSED))
            ...
    }
    

Also, instead of moving individual files with MoveFileExA(), consider using SHFileOperation() or IFileOperation::MoveItems() instead so you can move multiple files in a single operation.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • *that is why you see them as "in use".* no, src of error - `MoveFileEx` not move directory to different volume. the leaking open handles of course is error – RbMm Oct 05 '18 at 19:49
  • @RbMm `MoveFileEx` can move files and folders across *volumes* on the same *drive*, and move files across *drives* with `MOVEFILE_COPY_ALLOWED`, but it cannot move folders across *drives*. – Remy Lebeau Oct 05 '18 at 21:52
  • yes, exactly. but if we have folder and target on another volume - the `MoveFileEx` return status - `STATUS_FILE_IS_A_DIRECTORY` which is converted by win32 to error access denied. the op wrong interpret error. real error is `STATUS_FILE_IS_A_DIRECTORY` and this is because it try move directory across volume – RbMm Oct 05 '18 at 21:59
  • really we need yourself recursive move folder in this case (different volume target). also many another points must be in code - like check reparse points folders (for not move it targets too), remove read only attributes before try move/delete. and we not need create any list of files. all can be done in place – RbMm Oct 05 '18 at 22:02
  • *across volumes on the same drive* - no - you mean physical disk drive ? this not play any role. the `FILE_RENAME_INFORMATION` - *A file or directory can only be renamed within a volume. In other words, a rename operation cannot cause a file or directory to be moved to a different volume.* – RbMm Oct 05 '18 at 22:04
  • @RbMm the [`MoveFileEx` documentation](https://learn.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-movefileexa) says: "*When moving a file, the destination can be on a different file system or volume. If the destination is on another drive, you must set the MOVEFILE_COPY_ALLOWED flag in dwFlags. When moving a directory, the destination must be on the same drive.*" – Remy Lebeau Oct 06 '18 at 05:59
  • you mean *must be on the same drive.* ? the documentation wrong or not enough exactly here. the destination must be on the same **Volume**. because this implemented via `FileRenameInformation` and here is correct - *A file or directory can only be renamed within a volume. In other words, a rename operation cannot cause a file or directory to be moved to a different volume* – RbMm Oct 06 '18 at 06:55
  • but i mistake at begin on reason - not look that `c:` used in both paths - so volume in concrete code is same. and error here really `STATUS_ACCESS_DENIED` because existing open handles to file - *A file cannot be renamed if it has any open handles*. however ned call `MoveFile` only for top folders in this case, but not for all files/folders recurcive – RbMm Oct 06 '18 at 06:59
  • Thanks for all the info everyone, including the additional bug highlights. Really grateful. I'll implement the advice on Monday and mark the answer then. – Ben Oct 06 '18 at 16:04
  • Worked a charm. Thank you! I've up voted this answer. – Ben Oct 08 '18 at 08:22