2009-01-26 13 views
7

Jaki jest najlepszy sposób monitorowania dysków przed działaniem plików. Mam na myśli uzyskanie pełnej nazwy pliku (c: \ temp \ abc.txt), akcji (stworzonej/usuniętej/zmodyfikowanej/przemianowanej), a także użytkownika (użytkownika 1) i nazwy procesu (notepad.exe) powodującego plik (wiele usuń) działania.Monitorowanie plików/folderów

Słyszałem o niektórych interfejsach API i powiadomieniach o powłoce, ale nie mogłem ich użyć do wszystkich powyższych potrzeb.

Pozdrawiamy.

Odpowiedz

5

Istnieje system Windows API do odbierania powiadomień o zmianach w katalogach i opcjonalnych podkatalogach tego, patrz this SO question dla komponentu Delphi zawierającego API.

Istnieje jednak (AFAIK) brak gotowego interfejsu do uzyskania wszystkich potrzebnych informacji. Można oczywiście oglądać katalogi, zapełniać listy plików wszystkimi dostępnymi informacjami o każdym pliku i porównywać bieżącą listę z poprzednią listą, aby określić, co się zmieniło i kto to zrobił. Nie będzie to jednak skalowanie i nie będzie możliwości uzyskania wszystkich informacji - zobaczysz na przykład, że plik został usunięty, ale nie sądzę, że możesz uzyskać informacje, z którego konta użytkownika plik został usunięty .

Edit: Narzędzia, takie jak Process Explorer i przyjaciele dostarczyć dużo więcej informacji o tym co się dzieje w systemie, niż jest dostępne za pośrednictwem interfejsu API systemu Windows, ale często potrzebują one kierowcom dostęp do tego rodzaju informacji, i muszą być prowadzone z uprawnieniami administratora.

10

Jeden z moich ulubionych blogów odpowiedział na to pytanie (z pełnym źródłem i aplikacją demo) jakiś czas temu. Kasa Delphi About.com article here, która zawiera bardziej szczegółowe wyjaśnienie. Kod dostarczony przez Zarko Gajica pod adresem http://delphi.about.com

Chcesz otrzymywać powiadomienia o utworzeniu, zmianie nazwy lub usunięciu pliku w systemie? Chcesz znać dokładną nazwę folderu i pliku? Zacznijmy monitorować zmiany powłoki systemu!

//TSHChangeNotify 

unit SHChangeNotify; 

{$IFNDEF VER80} {$IFNDEF VER90} {$IFNDEF VER93} 
    {$DEFINE Delphi3orHigher} 
{$ENDIF} {$ENDIF} {$ENDIF} 

//************************************************************* 
//************************************************************* 
// TSHChangeNotify component by Elliott Shevin [email protected] 
// vers. 3.0, October 2000 
// 
// See the README.TXT file for revision history. 
// 
//* 
//* I owe this component to James Holderness, who described the 
//* use of the undocumented Windows API calls it depends upon, 
//* and Brad Martinez, who coded a similar function in Visual 
//* Basic. I quote here from Brad's expression of gratitude to 
//* James: 
//*  Interpretation of the shell's undocumented functions 
//*  SHChangeNotifyRegister (ordinal 2) and SHChangeNotifyDeregister 
//*  (ordinal 4) would not have been possible without the 
//*  assistance of James Holderness. For a complete (and probably 
//*  more accurate) overview of shell change notifcations, 
//*  please refer to James' "Shell Notifications" page at 
//*  http://www.geocities.com/SiliconValley/4942/ 
//* 
//* This component will let you know when selected events 
//* occur in the Windows shell, such as files and folders 
//* being renamed, added, or deleted. (Moving an item yields 
//* the same results as renaming it.) For the complete list 
//* of events the component can trap, see Win32 Programmer's 
//* reference description of the SHChangeNotify API call. 
//* 
//* Properties: 
//*  MessageNo: the Windows message number which will be used to signal 
//*    a trapped event. The default is WM_USER (1024); you may 
//*    set it to some other value if you're using WM_USER for 
//*    any other purpose. 
//*  TextCase: tcAsIs (default), tcLowercase, or tcUppercase, determines 
//*    whether and how the Path parameters passed to your event 
//*    handlers are case-converted. 
//*  HardDriveOnly: when set to True, the component monitors only local 
//*    hard drive partitions; when set to False, monitors the 
//*    entire file system. 
//* 
//* Methods: 
//*  Execute: Begin monitoring the selected shell events. 
//*  Stop:  Stop monitoring. 
//* 
//* Events: 
//*  The component has an event corresponding to each event it can 
//*  trap, e.g. OnCreate, OnMediaInsert, etc. 
//*  Each event handler is passed either three or four parameters-- 
//*   Sender=this component. 
//*   Flags=the value indentifying the event that triggered the handler, 
//*    from the constants in the SHChangeNotify help. This parameter 
//*    allows multiple events to share handlers and still distinguish 
//*    the reason the handler was triggered. 
//*   Path1, Path2: strings which are the paths affected by the shell 
//*    event. Whether both are passed depends on whether the second 
//*    is needed to describe the event. For example, OnDelete gives 
//*    only the name of the file (including path) that was deleted; 
//*    but OnRenameFolder gives the original folder name in Path1 
//*    and the new name in Path2. 
//*    In some cases, such as OnAssocChanged, neither Path parameter 
//*    means anything, and in other cases, I guessed, but we always 
//*    pass at least one. 
//*  Each time an event property is changed, the component is reset to 
//*  trap only those events for which handlers are assigned. So assigning 
//*  an event handler suffices to indicate your intention to trap the 
//*  corresponding shell event. 
//* 
//*  There is one more event: OnEndSessionQuery, which has the same 
//*  parameters as the standard Delphi OnCloseQuery (and can in fact 
//*  be your OnCloseQuery handler). This component must shut down its 
//*  interception of shell events when system shutdown is begun, lest 
//*  the system fail to shut down at the user's request. 
//* 
//*  Setting CanEndSession (same as CanClose) to FALSE in an 
//*  OnEndSessionQuery will stop the process of shutting down 
//*  Windows. You would only need this if you need to keep the user 
//*  from ending his Windows session while your program is running. 
//* 
//* I'd be honored to hear what you think of this component. 
//* You can write me at [email protected] 
//************************************************************* 
//************************************************************* 

interface 

uses 
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, 
    {$IFNDEF Delphi3orHigher} 
    OLE2, 
    {$ELSE} 
    ActiveX, ComObj, 
    {$ENDIF} 

    ShlObj; 

const 
    SHCNF_ACCEPT_INTERRUPTS  = $0001; 
    SHCNF_ACCEPT_NON_INTERRUPTS = $0002; 
    SHCNF_NO_PROXY    = $8000; 

type NOTIFYREGISTER = record 
    pidlPath  : PItemIDList; 
    bWatchSubtree : boolean; 
end; 

type PNOTIFYREGISTER = ^NOTIFYREGISTER; 

type TTextCase = (tcAsIs,tcUppercase,tcLowercase); 

type 
    TOneParmEvent = procedure(Sender : TObject; Flags : cardinal; Path1 : string) of object; 
    TTwoParmEvent = procedure(Sender : TObject; Flags : cardinal; Path1, Path2 : string) of object; 
    TEndSessionQueryEvent = procedure(Sender: TObject; var CanEndSession: Boolean) of object; 

    function SHChangeNotifyRegister(
     hWnd  : HWND; 
     dwFlags  : integer; 
     wEventMask : cardinal; 
     uMsg  : UINT; 
     cItems  : integer; 
     lpItems  : PNOTIFYREGISTER) : HWND; stdcall; 
    function SHChangeNotifyDeregister(
     hWnd  : HWND) : boolean; stdcall; 
    function SHILCreateFromPath(Path: Pointer; 
        PIDL: PItemIDList; var Attributes: ULONG): 
        HResult; stdcall; 


type 
    TSHChangeNotify = class(TComponent) 
    private 
    fTextCase  : TTextCase; 
    fHardDriveOnly : boolean; 
    NotifyCount : integer; 
    NotifyHandle : hwnd; 
    NotifyArray : array[1..26] of NOTIFYREGISTER; 
    AllocInterface : IMalloc; 
    PrevMsg  : integer; 
    prevpath1  : string; 
    prevpath2  : string; 
    fMessageNo  : integer; 
    fAssocChanged  : TTwoParmEvent; 
    fAttributes  : TOneParmEvent; 
    fCreate   : TOneParmEvent; 
    fDelete   : TOneParmEvent; 
    fDriveAdd   : TOneParmEvent; 
    fDriveAddGUI  : TOneParmEvent; 
    fDriveRemoved  : TOneParmEvent; 
    fMediaInserted : TOneParmEvent; 
    fMediaRemoved  : TOneParmEvent; 
    fMkDir   : TOneParmEvent; 
    fNetShare   : TOneParmEvent; 
    fNetUnshare  : TOneParmEvent; 
    fRenameFolder  : TTwoParmEvent; 
    fRenameItem  : TTwoParmEvent; 
    fRmDir   : TOneParmEvent; 
    fServerDisconnect : TOneParmEvent; 
    fUpdateDir  : TOneParmEvent; 
    fUpdateImage  : TOneParmEvent; 
    fUpdateItem  : TOneParmEvent; 
    fEndSessionQuery : TEndSessionQueryEvent; 

    OwnerWindowProc : TWndMethod; 

    procedure SetMessageNo(value : integer); 
    procedure WndProc(var msg: TMessage); 

    protected 
    procedure QueryEndSession(var msg: TMessage); 

    public 
    constructor Create(AOwner : TComponent); override; 
    destructor Destroy; override; 
    procedure Execute; 
    procedure Stop; 

    published 
    property MessageNo : integer read fMessageNo write SetMessageNo default WM_USER; 
    property TextCase : TTextCase read fTextCase write fTextCase default tcAsIs; 
    property HardDriveOnly : boolean read fHardDriveOnly write fHardDriveOnly default True; 

    property OnAssocChanged  : TTwoParmEvent read fAssocChanged write fAssocChanged; 
    property OnAttributes : TOneParmEvent read fAttributes write fAttributes; 
    property OnCreate   : TOneParmEvent read fCreate  write fCreate; 
    property OnDelete   : TOneParmEvent read fDelete  write fDelete; 
    property OnDriveAdd   : TOneParmEvent read fDriveAdd  write fDriveAdd; 
    property OnDriveAddGUI : TOneParmEvent read fDriveAddGUI write fDriveAddGUI; 
    property OnDriveRemoved : TOneParmEvent read fDriveRemoved write fDriveRemoved; 
    property OnMediaInserted : TOneParmEvent read fMediaInserted write fMediaInserted; 
    property OnMediaRemoved : TOneParmEvent read fMediaRemoved write fMediaRemoved; 
    property OnMkDir   : TOneParmEvent read fMkDir  write fMkDir; 
    property OnNetShare   : TOneParmEvent read fNetShare  write fNetShare; 
    property OnNetUnshare : TOneParmEvent read fNetUnshare write fNetUnshare; 
    property OnRenameFolder : TTwoParmEvent read fRenameFolder write fRenameFolder; 
    property OnRenameItem : TTwoParmEvent read fRenameItem write fRenameItem; 
    property OnRmDir   : TOneParmEvent read fRmDir  write fRmDir; 
    property OnServerDisconnect : TOneParmEvent read fServerDisconnect write fServerDisconnect; 
    property OnUpdateDir  : TOneParmEvent read fUpdateDir write fUpdateDir; 
    property OnUpdateImage : TOneParmEvent read fUpdateImage write fUpdateImage; 
    property OnUpdateItem  : TOneParmEvent read fUpdateItem write fUpdateItem; 
    property OnEndSessionQuery : TEndSessionQueryEvent 
             read fEndSessionQuery write fEndSessionQuery; 
    { Published declarations } 
    end; 

procedure Register; 

implementation 

const Shell32DLL = 'shell32.dll'; 

function SHChangeNotifyRegister; 
       external Shell32DLL index 2; 
function SHChangeNotifyDeregister; 
       external Shell32DLL index 4; 
function SHILCreateFromPath; 
       external Shell32DLL index 28; 

procedure Register; 
begin 
    RegisterComponents('Custom', [TSHChangeNotify]); 
end; 

// Set defaults, and ensure NotifyHandle is zero. 
constructor TSHChangeNotify.Create (AOwner : TComponent); 
begin 
    inherited Create(AOwner); 
    fTextCase  := tcAsIs; 
    fHardDriveOnly := true; 

    fAssocChanged  := nil; 
    fAttributes  := nil; 
    fCreate   := nil; 
    fDelete   := nil; 
    fDriveAdd   := nil; 
    fDriveAddGUI  := nil; 
    fDriveRemoved  := nil; 
    fMediaInserted := nil; 
    fMediaRemoved  := nil; 
    fMkDir   := nil; 
    fNetShare   := nil; 
    fNetUnshare  := nil; 
    fRenameFolder  := nil; 
    fRenameItem  := nil; 
    fRmDir   := nil; 
    fServerDisconnect := nil; 
    fUpdateDir  := nil; 
    fUpdateImage  := nil; 
    fUpdateItem  := nil; 
    fEndSessionQuery := nil; 

    MessageNo := WM_USER; 

    // If designing, dodge the code that implements messag interception. 
    if csDesigning in ComponentState 
     then exit; 

    // Substitute our window proc for our owner's window proc. 
    OwnerWindowProc := (Owner as TWinControl).WindowProc; 
    (Owner as TWinControl).WindowProc := WndProc; 

    // Get the IMAlloc interface so we can free PIDLs. 
    SHGetMalloc(AllocInterface); 
end; 

procedure TSHChangeNotify.SetMessageNo(value : integer); 
begin 
    if (value >= WM_USER) 
    then fMessageNo := value 
    else raise Exception.Create 
        ('MessageNo must be greater than or equal to ' 
        + inttostr(WM_USER)); 
end; 

// Execute unregisters any current notification and registers a new one. 
procedure TSHChangeNotify.Execute; 
var 
    EventMask  : integer; 
    driveletter : string; 
    i    : integer; 
    pidl   : PItemIDList; 
    Attributes  : ULONG; 
    NotifyPtr  : PNOTIFYREGISTER; 
begin 
    NotifyCount := 0; 

    if csDesigning in ComponentState 
     then exit; 

    Stop; // Unregister the current notification, if any. 

    EventMask := 0; 
    if assigned(fAssocChanged ) then EventMask := (EventMask or SHCNE_ASSOCCHANGED); 
    if assigned(fAttributes  ) then EventMask := (EventMask or SHCNE_ATTRIBUTES); 
    if assigned(fCreate   ) then EventMask := (EventMask or SHCNE_CREATE); 
    if assigned(fDelete   ) then EventMask := (EventMask or SHCNE_DELETE); 
    if assigned(fDriveAdd  ) then EventMask := (EventMask or SHCNE_DRIVEADD); 
    if assigned(fDriveAddGUI  ) then EventMask := (EventMask or SHCNE_DRIVEADDGUI); 
    if assigned(fDriveRemoved ) then EventMask := (EventMask or SHCNE_DRIVEREMOVED); 
    if assigned(fMediaInserted ) then EventMask := (EventMask or SHCNE_MEDIAINSERTED); 
    if assigned(fMediaRemoved ) then EventMask := (EventMask or SHCNE_MEDIAREMOVED); 
    if assigned(fMkDir   ) then EventMask := (EventMask or SHCNE_MKDIR); 
    if assigned(fNetShare  ) then EventMask := (EventMask or SHCNE_NETSHARE); 
    if assigned(fNetUnshare  ) then EventMask := (EventMask or SHCNE_NETUNSHARE); 
    if assigned(fRenameFolder ) then EventMask := (EventMask or SHCNE_RENAMEFOLDER); 
    if assigned(fRenameItem  ) then EventMask := (EventMask or SHCNE_RENAMEITEM); 
    if assigned(fRmDir   ) then EventMask := (EventMask or SHCNE_RMDIR); 
    if assigned(fServerDisconnect) then EventMask := (EventMask or SHCNE_SERVERDISCONNECT); 
    if assigned(fUpdateDir  ) then EventMask := (EventMask or SHCNE_UPDATEDIR); 
    if assigned(fUpdateImage  ) then EventMask := (EventMask or SHCNE_UPDATEIMAGE); 
    if assigned(fUpdateItem  ) then EventMask := (EventMask or SHCNE_UPDATEITEM); 

    if EventMask = 0 // If there's no event mask 
     then exit;  // then there's no need to set an event. 

    // If the user requests watches on hard drives only, cycle through 
    // the list of drive letters and add a NotifyList element for each. 
    // Otherwise, just set the first element to watch the entire file 
    // system. 
    if fHardDriveOnly 
    then for i := ord('A') to ord('Z') do begin 
      DriveLetter := char(i) + ':\'; 
      if GetDriveType(pchar(DriveLetter)) = DRIVE_FIXED 
       then begin 
         inc(NotifyCount); 
         with NotifyArray[NotifyCount] do begin 
          SHILCreateFromPath 
            (pchar(DriveLetter), 
             addr(pidl), 
             Attributes); 
          pidlPath := pidl; 
          bWatchSubtree := true; 
         end; 
      end; 
    end 

    // If the caller requests the entire file system be watched, 
    // prepare the first NotifyElement accordingly. 
    else begin 
      NotifyCount := 1; 
      with NotifyArray[1] do begin 
       pidlPath  := nil; 
       bWatchSubtree := true; 
      end; 
    end; 

    NotifyPtr := addr(NotifyArray); 

    NotifyHandle := SHChangeNotifyRegister(
           (Owner as TWinControl).Handle, 
           SHCNF_ACCEPT_INTERRUPTS  + 
            SHCNF_ACCEPT_NON_INTERRUPTS, 
           EventMask, 
           fMessageNo, 
           NotifyCount, 
           NotifyPtr); 

    if NotifyHandle = 0 
     then begin 
      Stop; 
      raise Exception.Create('Could not register SHChangeNotify'); 
    end; 
end; 

// This procedure unregisters the Change Notification 
procedure TSHChangeNotify.Stop; 
var 
    NotifyHandle : hwnd; 
    i    : integer; 
    pidl   : PITEMIDLIST; 
begin 
    if csDesigning in ComponentState 
     then exit; 

    // Deregister the shell notification. 
    if NotifyCount > 0 
     then SHChangeNotifyDeregister(NotifyHandle); 

    // Free the PIDLs in NotifyArray. 
    for i := 1 to NotifyCount do begin 
     pidl := NotifyArray[i].PidlPath; 
     if AllocInterface.DidAlloc(pidl) = 1 
         then AllocInterface.Free(pidl); 
    end; 

    NotifyCount := 0; 
end; 

// This is the procedure that is called when a change notification occurs. 
// It interprets the two PIDLs passed to it, and calls the appropriate 
// event handler, according to what kind of event occurred. 
procedure TSHChangeNotify.WndProc(var msg: TMessage); 
type 
    TPIDLLIST = record 
     pidlist : array[1..2] of PITEMIDLIST; 
    end; 
    PIDARRAY = ^TPIDLLIST; 
var 
    Path1 : string; 
    Path2 : string; 
    ptr  : PIDARRAY; 
    p1,p2 : PITEMIDLIST; 
    repeated : boolean; 
    p  : integer; 
    event : longint; 
    parmcount  : byte; 
    OneParmEvent : TOneParmEvent; 
    TwoParmEvent : TTwoParmEvent; 

    // The internal function ParsePidl returns the string corresponding 
    // to a PIDL. 
    function ParsePidl (Pidl : PITEMIDLIST) : string; 
    begin 
     SetLength(result,MAX_PATH); 
     if not SHGetPathFromIDList(Pidl,pchar(result)) 
      then result := ''; 
    end; 

// The actual message handler starts here. 
begin 
    if Msg.Msg = WM_QUERYENDSESSION 
    then QueryEndSession(Msg); 

    if Msg.Msg = fMessageNo 
    then begin 
     OneParmEvent := nil; 
     TwoParmEvent := nil; 

     event := msg.LParam and ($7FFFFFFF); 

     case event of 
      SHCNE_ASSOCCHANGED  : TwoParmEvent := fAssocChanged; 
      SHCNE_ATTRIBUTES  : OneParmEvent := fAttributes; 
      SHCNE_CREATE   : OneParmEvent := fCreate; 
      SHCNE_DELETE   : OneParmEvent := fDelete; 
      SHCNE_DRIVEADD   : OneParmEvent := fDriveAdd; 
      SHCNE_DRIVEADDGUI  : OneParmEvent := fDriveAddGUI; 
      SHCNE_DRIVEREMOVED  : OneParmEvent := fDriveRemoved; 
      SHCNE_MEDIAINSERTED : OneParmEvent := fMediaInserted; 
      SHCNE_MEDIAREMOVED  : OneParmEvent := fMediaRemoved; 
      SHCNE_MKDIR   : OneParmEvent := fMkDir; 
      SHCNE_NETSHARE   : OneParmEvent := fNetShare; 
      SHCNE_NETUNSHARE  : OneParmEvent := fNetUnshare; 
      SHCNE_RENAMEFOLDER  : TwoParmEvent := fRenameFolder; 
      SHCNE_RENAMEITEM  : TwoParmEvent := fRenameItem; 
      SHCNE_RMDIR   : OneParmEvent := fRmDir; 
      SHCNE_SERVERDISCONNECT : OneParmEvent := fServerDisconnect; 
      SHCNE_UPDATEDIR  : OneParmEvent := fUpdateDir; 
      SHCNE_UPDATEIMAGE  : OneParmEvent := fUpdateImage; 
      SHCNE_UPDATEITEM  : OneParmEvent := fUpdateItem; 
      else begin 
        OneParmEvent := nil; // Unknown event; 
        TwoParmEvent := nil; 
       end; 
     end; 
     if (assigned(OneParmEvent)) or (assigned(TwoParmEvent)) 
      then begin 

       // Assign a pointer to the array of PIDLs sent 
       // with the message. 
       ptr := PIDARRAY(msg.wParam); 

       // Parse the two PIDLs. 
       p1 := ptr^.pidlist[1]; 
       try 
        SetLength(Path1,MAX_PATH); 
        Path1 := ParsePidl(p1); 
        p := pos(#00,Path1); 
        if p > 0 
         then SetLength(Path1,p - 1); 
       except 
        Path1 := ''; 
       end; 

       p2 := ptr^.pidlist[2]; 
       try 
        SetLength(Path2,MAX_PATH); 
        Path2 := ParsePidl(p2); 
        p := pos(#00,Path2); 
        if p > 0 
         then SetLength(Path2,p - 1); 
       except 
        Path2 := ''; 
       end; 

       // If this message is the same as the last one (which happens 
       // a lot), bail out. 
       try 
        repeated := (PrevMsg = event) 
           and (uppercase(prevpath1) = uppercase(Path1)) 
           and (uppercase(prevpath2) = uppercase(Path2)) 
       except 
        repeated := false; 
       end; 

       // Save the elements of this message for comparison next time. 
       PrevMsg := event; 
       PrevPath1 := Path1; 
       PrevPath2 := Path2; 

       // Convert the case of Path1 and Path2 if desired. 
       case fTextCase of 
         tcUppercase : begin 
          Path1 := uppercase(Path1); 
          Path2 := uppercase(Path2); 
         end; 
         tcLowercase : begin 
          Path1 := lowercase(Path1); 
          Path2 := lowercase(Path2); 
         end; 
       end; 

       // Call the event handler according to the number 
       // of paths we will pass to it. 
       if not repeated then begin 
        case event of 
         SHCNE_ASSOCCHANGED, 
         SHCNE_RENAMEFOLDER, 
         SHCNE_RENAMEITEM : parmcount := 2; 
        else parmcount := 1; 
        end; 

        if parmcount = 1 
         then OneParmEvent(self, event, Path1) 
         else TwoParmEvent(self, event, Path1, Path2); 
       end; 

     end; // if assigned(OneParmEvent)... 

    end;  // if Msg.Msg = fMessageNo... 

    // Call the original message handler. 
    OwnerWindowProc(Msg); 
end; 

procedure TSHChangeNotify.QueryEndSession(var msg: TMessage); 
var 
    CanEndSession : boolean; 
begin 
    CanEndSession := true; 
    if Assigned(fEndSessionQuery) 
     then fEndSessionQuery(Self, CanEndSession); 
    if CanEndSession 
     then begin 
      Stop; 
      Msg.Result := 1; 
     end 
     else Msg.Result := 0; 
end; 

destructor TSHChangeNotify.Destroy; 
begin 
    if not (csDesigning in ComponentState) 
     then begin 
      if Assigned(Owner) 
       then (Owner as TWinControl).WindowProc := OwnerWindowProc; 
      Stop; 
    end; 

    inherited; 
end; 

end. 


{ 
******************************************** 
Zarko Gajic 
About.com Guide to Delphi Programming 
http://delphi.about.com 
email: [email protected] 
free newsletter: http://delphi.about.com/library/blnewsletter.htm 
forum: http://forums.about.com/ab-delphi/start/ 
******************************************** 
} 
+0

To może pokazać, które użytkownik zmienił plik? – NccWarp9