views:

434

answers:

4

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.

  1. 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)
  2. psexec \\<IPAddress> -i /accepteula -w "c:\windows\system32" cmd.exe "/c dir q:\video /OD /TC /B > q:\dir.txt"
  3. //Read the first line of "w:\dir.txt" to get the oldest file in that directory.
  4. //Disconnect from the remote computer using windows.WNetCancelConnection2
A: 

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.

Mason Wheeler
Why is this a better solution than FindFirst/FindNext?
Lasse V. Karlsen
Because FindFirst/FindNext is an iterative solution that keeps going back to the network drive for each file. This is just fine on a local machine, but when you're incurring significant delays over a network you want to get all the information in one step, only take the latency hit once, and then sort it out client-side.
Mason Wheeler
So you're saying that DIR has some other way of finding the contents of the network drive?
Lasse V. Karlsen
And you can run DIR by using the shell interpreter (there is no DOS commands left in todays windows versions), by executing `%COMSPEC% /C DIR`, that is, the contents of the environment variable "COMSPEC" contains the full path to and name of the command shell interpreter, and the /C parameter instructs it to execute the command and then terminate, instead of waiting for more commands.
Lasse V. Karlsen
@Lasse: Yes. It pulls the directory description list from the network drive all at once. When you open a shared folder in Windows, do the icons for the files and folders in it show up one at a time, or all together?
Mason Wheeler
Like using the SH* file operations for instance? I doubt DIR is using some magic way not available to programmers. I could be wrong, I just doubt it. There's plenty of different ways to access the directory structures, like the SH* operations, IShellFolder interface.
Lasse V. Karlsen
@Mason: I'm sorry, but you're wrong. Everything goes through FindFirstFile eventually and it handles the buffering internally. http://blogs.msdn.com/oldnewthing/archive/2006/04/06/569873.aspx
Craig Peterson
And more to the point, Raymond's response to the first comment here: http://blogs.msdn.com/oldnewthing/archive/2009/04/15/9549682.aspx
Craig Peterson
A: 

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.

Craig Peterson
Oh yes, there is a faster solution: Querying the Windows Search Index, which can be done remotely. But if Windows Search isn't active, I agree with you...
Mef
If you post an answer showing how to do that you've got my vote.
Craig Peterson
+2  A: 

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.

TOndrej
A: 

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.

ioan