Setting the file date on Indy's FTP Server component
In this entry, I discuss setting the file time on the Indy FTP server component.
Earlier, I blogged about the problems abusing the MDTM command to set the
file date. On a FTP Server, supporting the ability to set the file time is even worse.
The Indy FTP Server now supports three ways to set the time. The commands are MDTM, SITE UTIME,
and the MFx commands.
I will discuss each one of these individually. If you do NOT have the FTP
server, you can download it here.
MDTM
There is the usual abuse of the MDTM command. To support
this, you must use the OnSetModifiedTime event. It is defined like this:
procedure(ASender: TIdFTPServerContext; const AFileName : String; var AFileTime : TIdDateTime) of object;
The reason that AFileName is a variable parameter because another command I will mention later requires
you to return the time as it was set on the file system. You also should use the OnFileExistCheck
event which is defined like this: rocedure(ASender: TIdFTPServerContext; const APathName: string; var VExist : Boolean) of object;
The reason is that you have can have a hard time distinguishing betwen a get file date MDTM request with
a set file MDTM request if the file name starts with something that looks like a valid time stamp followed
by a space and a bunch of characters (remember that some file systems are extremely liberal about filenames).
You return True if the filename exists and the request is assumed to be a get file date request or if
you set VExists to false, the server assumes you want to set the file date on the filename after the time
stamp. You should not use Borland's SysUtil routines with this event because this event passes
a time stamp as GMT while Borland's SysUtil routines handle times in the user's local time zone.
You also have to return a time in GMT form. A code sample from the demo FTP server should help you handle
this:
procedure TForm1.IdFTPServer1SetModifiedTime(ASender: TIdFTPServerContext;
const AFileName: string; var AFileTime: TDateTime);
//This looks odd because you have to return the date after you modify it in the
//command reply.
//
//That's part of the MFF, MFCT, and MFMT commands which are defined by:
//
//http://ftp.netzmafia.de/rfc/internet-drafts/draft-somers-ftp-mfxx-01.txt
//http://www.trevezel.com/downloads/draft-somers-ftp-mfxx-00.html
//
//I know that this draft expired but it really is a shame because there is a large
//need for this. This is a more eligant solution than simply abusing the MDTM
//command to set a file date.
var LHandle : THandle;
LModDate : TFileTime;
begin
//We use the Win32 API instead of Borland's RTL because this is based on GMT.
//This is based on:
//http://www.swissdelphicenter.ch/torry/showcode.php?id=855
LHandle := CreateFile(PChar(ReplaceChars(AppDir+'\'+AFileName)),
GENERIC_READ or GENERIC_WRITE,
0,
nil,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
0);
if LHandle <> INVALID_HANDLE_VALUE then
begin
try
LModDate := TDateTimeToFileTime(AFileTime);
if Windows.SetFileTime(LHandle,nil,nil,@LModDate) then
begin
//now that we set the date, we need to return what it was
//set to because different file systems have different resolutions.
if Windows.GetFileTime(LHandle,nil,nil,@LModDate) then
begin
AFileTime := FileTimeToTDateTime(LModDate);
end;
end
else
begin
raise Exception.Create('File Operation Aborted');
end;
finally
CloseHandle(LHandle);
end;
end
end;
SITE UTIME
NcFTP and gFTP use a SITE
UTIME command. If you support the first feature, you also support this for setting the file's Last
Modified time. But this command is also used by NcFTP to set a file creation time and a file last
access time. The bad thing about this is that there are actually two syntaxes that I have seen.
NcFTP sends the command like this "SITE UTIME .bashrc 20050815165138 20050815081129 20050815081129 UTC"
while gFTP sends the command like this "SITE UTIME 20050815041129 /.bashrc" (the time stamp is the user's
local time). In the previous example commands, I refer to the same exact file. have
a special event in the FTP server called OnSiteUTIME which is defined like this:
procedure(ASender: TIdFTPServerContext; const AFileName : String;
var VLastAccessTime, VLastModTime, VCreateDate : TIdDateTime;
var VAUth : Boolean) of object;
This event is used not only to support the SITE UTIME command but also to set the file's Last Access time
with the MMF command. You should set VAuth to false if there is a permission problem.
Otherwise, just leave it set to true. Like the OnSetModifiedFile event, times are given and returned
as GMT. I admit that this event is not necessary but I provided it so that you could set file dates
in one Win32 API call (to reduce your I/O). One thing I need to note is that some timestamps are
"0" and that means that that particular time should not be set. I admit that it makes things
harder but I did because of how much the command varies. Here's some code from the sample FTP server: procedure TForm1.IdFTPServer1SiteUTIME(ASender: TIdFTPServerContext;
const AFileName: string; var VLastAccessTime, VLastModTime,
VCreateDate: TDateTime; var VAUth: Boolean);
var LHandle : THandle;
LModDate : TFileTime;
LPModDate : PFileTime;
LCreateDate :TFileTime;
LPCreateDate : PFileTime;
LAccessDate :TFileTime;
LPAccessDate : PFileTime;
begin
//We use the Win32 API instead of Borland's RTL because this is based on GMT.
//This is based on:
//http://www.swissdelphicenter.ch/torry/showcode.php?id=855
//We do things in a round-about way with pointers because sometimes a date will not
//be provided by a client and this could be used by the MFF command to set
//the Last Access Time fact
//The idea behind this event is to change the file dates in only ONE pass
LHandle := CreateFile(PChar(ReplaceChars(AppDir+'\'+AFileName)),
GENERIC_READ or GENERIC_WRITE,
0,
nil,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
0);
if LHandle <> INVALID_HANDLE_VALUE then
begin
try
if VCreateDate<>0 then
begin
LCreateDate := TDateTimeToFileTime(VCreateDate);
LPCreateDate := @LCreateDate;
end
else
begin
LPCreateDate := nil;
end;
if VLastModTime<>0 then
begin
LModDate := TDateTimeToFileTime(VLastModTime);
LPModDate := @LModDate;
end
else
begin
LPModDate := nil;
end;
if VLastAccessTime <> 0 then
begin
LAccessDate := TDateTimeToFileTime(VLastAccessTime);
LPAccessDate := @LAccessDate;
end
else
begin
LPAccessDate := nil;
end;
if Windows.SetFileTime(LHandle,LPCreateDate,LPAccessDate,@LModDate) then
begin
//now that we set the date, we need to return what it was
//set to because different file systems have different resolutions.
if Windows.GetFileTime(LHandle,@LCreateDate,@LAccessDate,@LModDate) then
begin
VLastModTime := FileTimeToTDateTime(LModDate);
VCreateDate := FileTimeToTDateTime(LCreateDate);
VLastAccessTime := FileTimeToTDateTime(LAccessDate);
end;
end
else
begin
raise Exception.Create('File Operation Aborted');
end;
finally
CloseHandle(LHandle);
end;
end
end;
MFMT and MFF Modify
This is my favorate because it is standardized with times given only with
GMT. If you support the MDTM command to set the file time, you also support this automatically.
It's handled transparently in the FTP server. There is, however, an optional OnSetCreationTime event
that is defined exactly like the OnSetModifiedTime and it works similarly (you just set the file creation
time instead of the file's last modification time). Here is some sample code from the demo
FTP Server:
procedure TForm1.IdFTPServer1SetCreationTime(ASender: TIdFTPServerContext;
const AFileName: string; var AFileTime: TDateTime);
var LHandle : THandle;
LCreateDate : TFileTime;
begin
//We use the Win32 API instead of Borland's RTL because this is based on GMT.
//This is based on:
//http://www.swissdelphicenter.ch/torry/showcode.php?id=855
LHandle := CreateFile(PChar( ReplaceChars(AppDir+'\'+AFileName)),
GENERIC_READ or GENERIC_WRITE,
0,
nil,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
0);
if LHandle <> INVALID_HANDLE_VALUE then
begin
try
LCreateDate := TDateTimeToFileTime(AFileTime);
if Windows.SetFileTime(LHandle,@LCreateDate,nil,nil) then
begin
//now that we set the date, we need to return what it was
//set to because different file systems have different resolutions.
if Windows.GetFileTime(LHandle,@LCreateDate,nil,nil) then
begin
AFileTime := FileTimeToTDateTime(LCreateDate);
end;
end
else
begin
raise Exception.Create('File Operation Aborted');
end;
finally
CloseHandle(LHandle);
end;
end
end;
On a Linux file system, you probably not use this event but on Win32, you might particularly as the MLST/MLSD
commands can return the file's creation time.
Conclusion (or my little soapbox)
Personally, I think what I have described is a mess that should not event exist when you want to do something
so simple as set a file's modification time. If we only used the "MFMT" or "MFF Modify" commands,
we would save a lot of problems.
If anything, I have probably understated the value of the MFF command in
this blog entry. That command is an attempt to provide a flexible standardized way to modify information
about a file such as file ownership (CHOWN), file group ownership (CHGRP), file permissions (CHMOD), and
file attributes in Windows (ATTRIB). In fact, you could modify all of that stuff in one single command.
This is one thing that makes SSH's file transfer (SFTP) a very robust protocol. I wish more servers
would support this feature better than they do today and I wish that IETF doesn't sit on this.