Win32 CreateDirectory fails with multiple folders in a long path - delphi

The environment is Windows 7 Pro and Delphi 7.
Windows.CreateDirectory() is failing to create multiple folders in a very long path that is well under the path length limit. GetLastError() returns ERROR_PATH_NOT_FOUND.
The failure is the same on an ESXi virtual machine, as well as a native Win7 workstation and physical disk. A similar failure occurs with Windows.MoveFile().
The long path in the code below is created correctly in a CMD window as a pasted parameter to MKDIR.
My work-around is creating this long path piece-meal. I split the path at the '\' character into a string array. Then I loop through the array and build the cumulative path from each element. The loop correctly builds the full path without an error.
I have no clue why the Win32 function fails to create a valid long path.
var
arrDstPath : TStringArray;
begin
// --------------
// failing method
// --------------
strDstPath := 'C:\Duplicate Files\my customer recovered data\desktop\my customer name\application data\gtek\gtupdate\aupdate\channels\ch_u3\html\images\';
if (Windows.CreateDirectory(pchar(strDstPath),nil) = false) then
Result := Windows.GetLastError; // #3 is returned
if (DirectoryExists(strNewPath) = false) then
Result := ERROR_PATH_NOT_FOUND;
// -----------------
// successful method
// -----------------
strNewPath := '';
LibSplitToArray(arrDstPath,'\',strDstPath);
for intIdx := 0 to High(arrDstPath) do
begin
strNewPath := strNewPath + arrDstPath[intIdx] + '\';
Windows.CreateDirectory(PChar(strNewPath), nil);
end;
if (DirectoryExists(strDstPath) = false) then // compare to original path string
begin
Result := ERROR_PATH_NOT_FOUND;
Exit;
end;

Actually, the official documentation for the CreateDirectory function describes what's going on. Since the function fails, your instinct should be to have a look at the section describing the return value, which states:
ERROR_ALREADY_EXISTS
The specified directory already exists.
ERROR_PATH_NOT_FOUND
One or more intermediate directories do not exist; this function will only create the final directory in the path.
I assume you got ERROR_PATH_NOT_FOUND, and the documentation suggests a probable reason: you are attempting to create several levels of subdirectories at once, which the function doesn't support.
Fortunately, the Delphi RTL has the ForceDirectories function that can create subdirectories recursively. (How can I Create folders recursively in Delphi?)
In Delphi 2010 and later, you can also use TDirectory.CreateDirectory from IOUtils.pas. Internally, this calls ForceDirectories.

Related

Application cant access file on startup

I'm doing an application(XE6 , Firemonkey) to synchronize files between a shared folder and a computer/s. This application checks every x hours if there are new files to be synchronized, and it starts on windows start-up.
I can do everything, my application starts on start-up, and it does the synchronization, as long as i'm the one starting it. Whem the application auto starts on start up it gives me an exception "EINOUTERROR" - File Access Denied.
On starting the application reads a small .txt file to set up it self (shared folder location, rate of synchronization etc), my guess is that since its the windows starting the app runs it without privileges to read the .txt, but even after changing the .txt permissions to full control on everyone it gives the same error.
File open code:
AssignFile(myFile,'Dados.txt');
if FileExists('Dados.txt') then
Append(myFile)
else
Rewrite(myFile);
FileMode := fmOpenRead;
Reset(myFile);
Code of placing the app on startup programs :
procedure TSyncM.RunOnStartup(const sCmdLine: string; bRunOnce: boolean; Remove: Boolean) ;
var sKey: string;
Section: string;
const ApplicationTitle = 'GEN4Sync';
begin
if (bRunOnce) then
sKey := 'Once'
else
sKey := '';
Section := 'Software\Microsoft\Windows\CurrentVersion\Run' + sKey + #0;
with TRegIniFile.Create('') do
try
RootKey := HKEY_CURRENT_USER;
if Remove then
DeleteKey(Section, ApplicationTitle)
else
WriteString(Section, ApplicationTitle, sCmdLine) ;
finally
Free;
end;
end;
If i comment the piece of code that calls the reading of that .txt my app starts and executes well, but i don't want to set it up everytime.
Thanks in advance
I think that the issue is related to your use of relative paths. You have written the code under the assumption that the working directory is the same directory as contains the executable. That is not necessarily so.
When you start the application by double clicking on the executable file, for instance, the shell ensures that the initial working directory is the directory containing the executable file. However, when Windows starts your program at startup I suspect that the working directory is the system directory. And of course your file is not found there, and you don't have rights to write there.
Instead of using relative paths, use the full path to the file.
FileName := ExtractFilePath(ParamStr(0)) + 'Dados.txt';
Or perhaps
FileName := TPath.Combine(ExtractFilePath(ParamStr(0)), 'Dados.txt');
Note that this does also assume that your executable file is located in a folder which you can write to. That is often not the case so you may need to find a different location.
I do have to comment that I find it somewhat incongruous that you are mixing the very modern (FireMonkey) with the ancient (Pascal I/O). Perhaps it is time to move to a more modern I/O technique.

FileGetDate works some times, thoughts?

Some files this works with and others it does not.
var
Src : integer;
FileDate : LongInt;
begin
Src:=FileOpen(SrcPath,fmOpenRead);
FileDate:=FileGetDate(Src); // Crash here with FileDate = -1
...
FileSetDate(Dest,FileDate);
I have checked Attributes for files that work and some that do not and they are identical.
Same for "Security," identical.
"Src" is a valid Integer for the ones that work and the ones that do not work.
The only thing I can see is that the full path to the ones that do not can be 130 characters and longer. But I renamed some Folders and shortened that to 118 and still no good.
Got me baffled. In a 2000+ file copy process, just 149 all in the same sub-Folder crash at this FileGetDate.
Any suggestions?
Thanks
The call to FileGetDate returns -1. The documentation says this:
The return value is -1 if the handle is invalid.
In other words, the handle returned by your call to FileOpen is not valid. You don't check for any errors in the code. Your code makes the assumption that all the calls succeed. The failure mode for FileOpen is that it returns -1. You are not checking the return value of FileOpen. You must add code to do so.
Note that the documentation for FileOpen says:
Note: We do not encourage the use of the non-native Delphi language file handlers such as FileOpen. These routines map to system
routines and return OS file handles, not normal Delphi file variables.
These are low-level file access routines. For normal file operations
use AssignFile, Rewrite, and Reset instead.
So even ancient legacy Pascal I/O is to be preferred to FileOpen.
Frankly, if you want to work with files and get meaningful error diagnostics, you should use the Win32 API. Call CreateFile and if it fails, check GetLastError to find out why. There are lots of ways in which a file open request can fail and realistically only you can work out what the reason is for your files. We don't have the files at hand, only you do.
Finally, you say that you are writing a file copy routine. The system already provides such a thing, and you would be far better off using it. You are spending a lot of effort re-inventing the wheel. What's more, writing a good file copy function is hard. The one that the system provides is known to work. Your version is liable to be inferior.
To copy a single file you can use CopyFile or CopyGFileEx. But you are copying multiple files and SHFileOperation is the API for that.
3 thoughts,
The first is that something else has exclusive access to the file and you simply can not open it regardless. Check then your opened file handle is valid.
The second though is that some files can have VERY damaged time stamps on them. I am not sure how it happens, I just know that it does.
Finally, according to the documents on Linux, -1 is a valid date value, you do not mention what file system your source files are stored on.
Here is the implementation of FileGetDate() in Delphi 5:
function FileGetDate(Handle: Integer): Integer;
var
FileTime, LocalFileTime: TFileTime;
begin
if GetFileTime(THandle(Handle), nil, nil, #FileTime) and
FileTimeToLocalFileTime(FileTime, LocalFileTime) and
FileTimeToDosDateTime(LocalFileTime, LongRec(Result).Hi,
LongRec(Result).Lo) then Exit;
Result := -1;
end;
That is 3 different points of failure that could happen on any given input file handle:
does GetFileTime() fail?
does FileTimeToLocalFileTime() fail?
does FileTimeToDosDateTime() fail?
Unless FileOpen() fails (which you are not checking for - it returns -1 if it is not able to open the file), then it is unlikely (but not impossible) that #1 or #2 are failing. But #3 does have a documented caveat:
The MS-DOS date format can represent only dates between 1/1/1980 and 12/31/2107; this conversion fails if the input file time is outside this range.
It is not likely that you encounter files with timestamps in the year 2108 and later, but you can certainly encounter files with timestamps in the year 1979 and earlier.
All 4 functions (counting the CreateFile() function called inside of FileOpen()) report an error code via GetLastError(), so you can do this:
var
Src : integer;
FileDate : LongInt;
begin
Src := FileOpen(SrcPath, fmOpenRead);
Win32Check(Src <> -1);
FileDate := FileGetDate(Src);
Win32Check(FileDate <> -1);
...
Win32Check(FileSetDate(Dest, FileDate) = 0);
Win32Check() calls RaiseLastWin32Error() if the input parameter is false. RaiseLastWin32Error() raises an EOSError exception containing the actual error code in its ErrorCode property.
If FileGetDate() fails, obviously you won't know which Win32 function actually failed. That is where the debugger comes into play. Enable Debug DCUs in your Project Options to allow you to step into the VCL/RTL source code. Find a file that fails, call FileGetDate() on it, and step through its source code to see which if the three API functions is actually failing.
Similarly for FileSetDate(), which also calls 3 API functions internally:
function FileSetDate(Handle: Integer; Age: Integer): Integer;
var
LocalFileTime, FileTime: TFileTime;
begin
Result := 0;
if DosDateTimeToFileTime(LongRec(Age).Hi, LongRec(Age).Lo, LocalFileTime) and
LocalFileTimeToFileTime(LocalFileTime, FileTime) and
SetFileTime(Handle, nil, nil, #FileTime) then Exit;
Result := GetLastError;
end;
If FileSetDate() fails, is it because:
DosDateTimeToFileTime() failed?
LocalFileTimeToFileTime() failed?
does SetFileTime() failed?

Is there a way to get my delphi program to wait for a large number of files to be copied?

I have written a program that does the following...
Monitors a folder for the creation of a new file with a specific filename that will eventually be created in a sub folder.
On creation of the file, the sub folders path is added to a queue in the form of a TList.
The files must be processed in the creation order.
A procedure is called to process all the files (images in this case) in the subfolder which involves moving the files to a network location.
The subfolder path is removed from the queue (TList).
If any more paths exist in the queue, the next path is passed to the processing procedure.
The problem I am having is that the time to copy the files to a network location varies depending on the number and size of the images so...
Is there a way to get Delphi to wait for procedure of file operation to finish?
I tried a while loop that waited for a boolean value to change (changed when the last file to be copied was found on the network) but that hung the application (even with application.processMessage) and the dirMonitor component failed to add the next sub folder to the TList.
Any suggestions would be most appreciated.
Thanks in advance.
Thanks for the replys...
I had a look at OmniThread which looks ideal... although I only have access to Delphi 7 so its a no go.
The problem Im having is that the folders take varying amounts of time to transfer due to differing sizes and network traffic etc... When a folder with a lot of images is followed by a folder with only a few images, the smaller of the two is reaching the network destination first. The network desination being a third party print spooler so the prints come off in the wrong order.
The simplified code:
procedure TForm1.programTimerTimer(Sender: TObject);
begin
if (fileOperationInProgress = false) AND (programPaused = false) then
begin
processOrderQueue;
end;
end;
procedure TForm1.processOrderQueue;
begin
// gets folder paths from queue
// processes images
// copy to print spooler (network location)
copyFolder(fromPath, toPath);
// remove temp files
end;
procedure TForm1.copyFolder(copyFrom : String; copyTo : String);
var
fos : TSHFileOpStruct;
begin
fileOperationInProgress := True;
ZeroMemory(#fos, SizeOf(fos));
with fos do
begin
wFunc := FO_COPY;
fFlags := FOF_FILESONLY or FOF_SILENT;
pFrom := PChar(copyFrom);
pTo := PChar(copyTo)
end;
ShFileOperation(fos);
fileOperationInProgress := False;
end;
Think I've come up with the answer... I'm going to do all file operationions in a single thread and set a global 'busy' boolean when it starts and change it again on completion.
That way the shell monitor won't miss messages when any file operations are in progress.
You could implement a file system watch. Essentially, you create a file handle with the following flags:
CreateFile(PChar(FDirectoryToWatch), FILE_LIST_DIRECTORY or GENERIC_READ,
FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, nil, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OVERLAPPED, 0);
and then create a loop to call ReadDirectoryChangesW.
Linked is an example class:
Why does ReadDirectoryChangesW omit events?
The only thing I would do differently is provide an event in the creation of the class to notify of changes (remembering that when calling the event in the Execute procedure it probably needs to be Synchronized).

How to fix madExcept creating temporal files in User\LocalSettings\Temp

I sterated using "Standard User Analyzer" from Application Compatibility toolkit and it reported that my app is not UAC compatible because:
"DeleteFileA: File (\Device\HarddiskVolume1\Documents and Settings\Administrator\Local Settings\Temp\mtgstudio.madExcept) is denied 'DELETE' access with error 0x5."
"DeleteFileA: File (\Device\HarddiskVolume1\Documents and Settings\Administrator\Local Settings\Temp) is denied 'DELETE' access with error 0x5."
Checking the madExcept.pas file I found:
function GetTempPath : AnsiString;
var arrCh : array [0..MAX_PATH] of AnsiChar;
begin
if windows.GetTempPathA(MAX_PATH, arrCh) > 0 then begin
result := arrCh;
if result <> '' then begin
CreateDirectoryA(PAnsiChar(result), nil);
if result[Length(result)] <> '\' then
result := result + '\';
result := result + KillExt(ExtractFileName(ModuleName(0))) + '.madExcept';
CreateDirectoryA(PAnsiChar(result), nil);
result := result + '\';
end;
end else
result := '';
end;
Is there a good way to overwrite the madExcept behaviour and store the temp files in a UAC allowed location?
It doesn't look like there's anything to fix. The GetTempPath API function is exactly the function to use to get a location where a program is allowed to create temporary files. That the compatibility tester was unable to delete the directories doesn't mean that the directories should have been someplace else. It only means they couldn't be deleted at the time the program tried. It could be that another program (such as the one being tested) had a file open in one of those directories; Windows doesn't allow folders to be deleted when there are open files in them.
One possible source of problems is the way MadExcept creates the directories. It creates them such that they inherit the permissions of their parent directories. If deletion is forbidden for the parent directory, then it will also be forbidden for the newly created temp directories. That partly points to a configuration problem on your system: GetTempPath might be returning a path for a directory that doesn't exist. It just returns the first value it finds in any of the TMP, TEMP, and USERPROFILE environment variables. It's the user's responsibility (not your program's) to make sure those are accurate.
Knowing that MadExcept uses GetTempPath to discover the temp directory gives you an opportunity. You can call SetEnvironmentVariable to change the TMP value for your process, and MadExcept will create its directory there instead. (But if the system-designated location for temporary files already doesn't work, good luck finding some alternative to use.)

Quickest way to find the oldest file in a directory using Delphi

HI
We have a large number of remote computers that capture video onto disk drives. Each camera has it's own unique directory and there can be up to 16 directories on any one disk.
I'm trying to locate the oldest video file on the disk but using FindFirst/FindNext to compare the File Creation DateTime takes forever.
Does anybody know of a more efficient way of finding the oldest file in a directory? We remotely connect to the pc's from a central HO location.
Regards, Pieter
-- Update
Thank you all for the answers. In the end I used the following.
Map a drive ('w:') to the remote computer using windows.WNetAddConnection2
//Execute dir on the remote computer using cmd.exe /c dir
//NOTE: Drive letters are relative to the remote computer. (psexec -w parameter)
psexec \\<IPAddress> -i /accepteula -w "c:\windows\system32" cmd.exe "/c dir q:\video /OD /TC /B > q:\dir.txt"
//Read the first line of "w:\dir.txt" to get the oldest file in that directory.
//Disconnect from the remote computer using windows.WNetCancelConnection2
You could also try FindFirstFileEx with FindExInfoBasic parameter, and on Windows 7 or Server 2008 R2 or later, FIND_FIRST_EX_LARGE_FETCH which should improve performance.
First, grab the RunDosAppPipedToTStrings routine from this page on how to run a DOS program and pipe its output to a TStrings. The example uses a TMemo's Lines property, but you can pass any TStrings in, such as TStringList. Note that this will fail silently if CreateProcess returns false. You might want to add an else case to the "if CreateProcess" block that raises an exception.
Then create a simple batch file in the same folder as your EXE. Call it getdir.bat. All it should say is:
dir %1
This produces a directory listing of whatever folder you pass to it. Unfortunately, "dir" is a DOS keyword command, not a program, so you can't invoke it directly. Wrapping it in a batch file gets around that. This is a bit of a hack, but it works. If you can find a better way to run DIR, so much the better.
You'll want to invoke RunDosAppPipedToTStrings with code that looks something like this:
procedure GetDirListing(dirname: string; list: TStringList);
const
CMDNAME = '%s\getdir.bat "%s"';
var
path: string;
begin
list.Clear;
path := ExcludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
RunDosAppPipedToTStrings(format(CMDNAME, [path, dirname]), list, false);
end;
Then all that's left to do is parse the output, extract date and time and filenames, sort by date and time, and grab the filename of the file with the lowest date. I'll leave that much to you.
If you can run something on the remote computer that can iterate over the directories, that will be the fastest approach. If you wanted to use Mason's example, try launching it with PsExec from SysInternals.
If you can only run an application locally then no, there's no faster way than FindFirst/FindNext, and anything else you do will boil down to that eventually. If your local computer is running Windows 7 you can use FindFirstFileEx instead, which has flags to indicate it should use larger buffers for the transfers and that it shouldn't read the 8.3 alias, which can help the speed a bit.
I had almost the same problem on the fax server software I developed. I had to send the faxes in the order they were received from thousands (all stored in a directory). The solution I adopted (which is slow to start but fast to run) is to make a sorted list of all the files using the
SearchRec.Time
as the key. After the file is in the list, I'm setting the attributes of the file as a faSysFile:
NewAttributes := Attributes or faSysFile;
Now when I do a new search with
FileAttrs := (faAnyFile and not faDirectory);
only the files that are not faSysFile are shown, so I can add to the list the files that are coming in new.
Now you have a list with all the files sorted by time.
Don't forget, when you start your application, first step is to remove the faSysFile attribute from the files in the folder so they can be processed again.
procedure FileSetSysAttr(AFileName: string);
var
Attributes, NewAttributes: Word;
begin
Attributes := FileGetAttr(AFileName);
NewAttributes := Attributes or faSysFile;
FileSetAttr(AFileName, NewAttributes);
end;
procedure FileUnSetSysAttr(AFileName: string);
var
Attributes, NewAttributes: Word;
begin
Attributes := FileGetAttr(AFileName);
NewAttributes := Attributes and not faSysFile;
FileSetAttr(AFileName, NewAttributes);
end;
procedure PathUnSetSysAttr(APathName: string);
var
sr: TSearchRec;
FileAttrs: Integer;
begin
FileAttrs := (faAnyFile and not faDirectory) and (faAnyFile or faSysFile);
APathName := IncludeTrailingBackslash(APathName);
if SysUtils.FindFirst(APathName + '*.*', FileAttrs, sr) = 0 then
try
repeat
if (sr.Attr and faDirectory) = 0 then
FileUnSetSysAttr(APathName + sr.Name);
until SysUtils.FindNext(sr) <> 0;
finally
SysUtils.FindClose(sr);
end;
end;
I know this is not the best solution, but works for me.

Resources