Jak odczytać plik pod Windows bez zmiany daty ostatniego dostępu?

0

Witam,

Jestem tu nowy. Kiedyś (jeszcze za czasów DOS-a) trochę programowałem w Turbo Pascalu. Teraz, na stare lata trochę mi odbiło i postanowiłem spróbować wrócić do pisania programów. Tym razem korzystam z Lazarusa.

Chcę spróbować napisać własny program pod Windows do robienia backupu plików, bo nie znalazłem do tej pory programu spełniającego moje wymagania.

Na razie walczę z kopiowaniem plików (żeby to było szybkie chcę korzystać z procedur blockread i blockwrite, docelowo chcę zapis i odczyt dać w dwóch wątkach, żeby jednocześnie dało się odczytywać dane z dysku źródłowego i zapisywać na docelowym). Jednak mam problem, którego nie potrafię na razie rozwiązać. Chodzi o to, że odczytując plik mimowolnie zmieniam jego czas ostatniego dostępu.

Znalazłem informację, że w celu zachowania oryginalnego czasu dostępu należy natychmiast po otwarciu pliku użyć funkcji SetFileTime z czasem ostatniego dostępu wynoszącym $0FFFFFFFF, $0FFFFFFFF. Link do artykułu z wyjaśnieniem: link.

Jednak mam pewien problem. Do odczytu pliku źródłowego przez BlockRead otwieram go procedurą Reset. Przed tą procedurą wykonuję oczywiście AssignFile(source,NazwaPlikuZrodlowego). Jednak do użycia funkcji SetFileTime powinienem użyć filehandle:=FileOpen(PlikDocelowy, fmOpenReadWrite or fmShareExclusive). Nie wiem, jak to pogodzić. Czy ktoś z Was mógłby mnie w tym temacie oświecić?

Poniżej zamieszczam kompletną procedurę kopiowania pliku z mojego programu (jest to moja aktualna wersja robocza):

  procedure KopiujPlik;
  var
     fileHandle            : cardinal;
     fsCreationTime        : TFileTime;
     fsLastAccessTime      : TFileTime;
     fsLastModificationTime: TFileTime;

     CzasStartu,CzasPracy:TDateTime;
     CzasPracyString,OldCzasPracyString:string;
     SourceFile,TargetFile:file;
     cntRead,cntWrite:word;
     Buffer:array[0..511] of byte;
     SizeOfFile,Skopiowano,temp:Int64;
     PromilPostepu,LastPromilPostepu:Integer;
     atrybuty,DataPliku:LongInt;
  begin
    CzasStartu:=now;
    OldCzasPracyString:='0';
    CzasPracy:=0;
    Skopiowano:=0;
    PromilPostepu:=0;
    LastPromilPostepu:=-1;
    FileMode:=0;
    AssignFile (SourceFile,NazwaPliku);
{    filehandle:=FileOpen(NazwaPliku, fmOpenReadWrite or fmShareExclusive);
    fsLastAccessTime.dwHighDateTime:=$0FFFFFFFF;
    fsLastAccessTime.dwLowDateTime:=$0FFFFFFFF;}
    Reset(SourceFile,1);
{    SetFileTime(fileHandle, nil, @fsLastAccessTime, nil);
    CloseHandle(fileHandle);}
    AssignFile (TargetFile,PlikDocelowy);
    Rewrite(TargetFile,1);
    SizeOfFile:=FileSize(SourceFile);
    repeat
      BlockRead(SourceFile,Buffer,SizeOf(Buffer),cntRead);
      BlockWrite(TargetFile,Buffer,cntRead,cntWrite);
      Skopiowano:=Skopiowano+cntWrite;
      temp:=Skopiowano*1000 div SizeOfFile;
      PromilPostepu:=temp;
      if(LastPromilPostepu<>PromilPostepu) then
         begin
           ProgressBar1.Position:=PromilPostepu;
           LastPromilPostepu:=PromilPostepu;
           CzasPracy:=now-CzasStartu;
           CzasPracyString:=FormatDateTime('hh:nn:ss',CzasPracy);
           if(CzasPracyString<>OldCzasPracyString) then
              begin
                Czasomierz.Caption:=CzasPracyString;
                Refresh;
                OldCzasPracyString:=CzasPracyString;
              end;
         end;
    until (cntRead=0) or (CntWrite<>cntRead);
    CloseFile(SourceFile);
    CloseFile(TargetFile);
    fileHandle:=CreateFile(PChar(NazwaPliku), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0);
    GetFileTime(fileHandle, @fsCreationTime, @fsLastAccessTime, @fsLastModificationTime);
{    DataPliku:=FileAge(NazwaPliku);
    FileSetDate(PlikDocelowy,DataPliku);}
    CloseHandle(fileHandle);

{    fileHandle:=CreateFile(PChar(PlikDocelowy), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0);}
    filehandle:=FileOpen(PlikDocelowy, fmOpenReadWrite or fmShareExclusive);
    SetFileTime(fileHandle, @fsCreationTime, @fsLastAccessTime, @fsLastModificationTime);
{    SetFileTime(fileHandle, @fsCreationTime, nil, nil);
    SetFileTime(fileHandle, nil, @fsLastAccessTime, nil);
    SetFileTime(fileHandle, nil, nil, @fsLastModificationTime);}
{    SetFileTime(fileHandle, @fsLastModificationTime, @fsLastModificationTime, @fsLastModificationTime);}
    CloseHandle(fileHandle);

    atrybuty:=FileGetAttr(NazwaPliku);
{    FileSetAttr(PlikDocelowy,atrybuty);}
    CzasPracy:=now-CzasStartu;
    Czasomierz.Caption:=FormatDateTime('hh:nn:ss',CzasPracy);
  end; 

0

Jeśli w BlockRead z dumą upatrujesz kosmicznej technologi i powalenia na kolana konkurencyjnych programów, to mam zła wiadomość .....

BlockRead

bo nie znalazłem do tej pory programu spełniającego moje wymagania.

A jakie one są?
Trening, czy umiesz zaprojektować założenia

Osobiście wyczuwam postawę: napiszę lepsze niż te na rynku, przy czym rynku nie znam.

0
AnyKtokolwiek napisał(a):

Jeśli w BlockRead z dumą upatrujesz kosmicznej technologi i powalenia na kolana konkurencyjnych programów, to mam zła wiadomość .....

A kto mówi o kosmicznych technologiach? Po prostu chcę napisać coś w miarę szybkiego, na ile pozwolą dyski, ale nie chcę schodzić aż do zapisu/odczytu sektorów na dysku.

AnyKtokolwiek napisał(a):

bo nie znalazłem do tej pory programu spełniającego moje wymagania.

A jakie one są?

Przede wszystkim chodzi mi o coś, co zminimalizuje czas wymagany na podłączenie zewnętrznego dysku USB do komputera. Etap przygotowawczy może się odbywać bez podłączania dysku backupu (na podstawie zapisanej uprzednio listy plików już obecnych na dysku backupowym). Poza tym restart komputera nie może być powodem konieczności zaczynania backupu od początku. Chcę robić backup plików z wybranych folderów (lub całych dysków) do zdefiniowanych folderów na dysku USB, a jeśli jakiś plik się zmieni to poprzednia wersja będzie przenoszona do oznaczonego datą foldera zawierającego wersje archiwalne, a zamiast niej we właściwym folderze pojawi się wersja najnowsza. Jeszcze parę innych rzeczy by się przydało, ale to jest mój cel minimum.

0

Domyślnie jest by default modyfikacja timestampów plików, przy dostępie, modyfikacji, utworzeniu.

Można wyłączyć tą opcję przy montowaniu dysku, noatime opcja na linuxie.

A tak można zmodyfikować timestamp pliku za pomocą winapi:
GetLastAccessTime(String)
GetFileTime
SetFileTime
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfiletime

Możesz ustawić poprzedni timestamp, ale wcześniej pobierasz aktualny timestamp.

Czytałem czy CreateFile ma jakieś flagi do ustawienia, które mogłby powodować pożądane zachowanie, ale niczego nie znalazłem.

Z zewnętrznych sposobów windows oferuje jeszcze modyfikację metadanych plików za pomocą powershella

(Get-Item file.txt).LastWriteTime=$(Get-Date "01/02/2001 01:30 pm")

Na linuxie jest program touch:
można touch -t rokmiesiącdzieńczas file.txt

touch -t 200101120000 file.txt

to zmieni na 12 stycznia 2001.

A programistycznie można użyć utimensat syscalla, który umożliwia modyifkację.

0

@tomac nie lepiej czas przeznaczyć na coś innego i skorzystać z gotowych rozwiązań? Są darmowe programy/systemy i umożliwiają tworzenie kopii różnicowej na poziomie fragmentów plików - gdy masz duży plik, i coś w nim zmieni się, to następna kopia nie jest całego pliku, tylko fragmentu obejmującego zmiany.
Ja używam od lat Ferro Backup System (kopia 2 komputerów za darmo) i jestem b.zadowolony.

1
tomac napisał(a):

Jednak mam pewien problem. Do odczytu pliku źródłowego przez BlockRead otwieram go procedurą Reset. Przed tą procedurą wykonuję oczywiście AssignFile(source,NazwaPlikuZrodlowego). Jednak do użycia funkcji SetFileTime powinienem użyć filehandle:=FileOpen(PlikDocelowy, fmOpenReadWrite or fmShareExclusive). Nie wiem, jak to pogodzić. Czy ktoś z Was mógłby mnie w tym temacie oświecić?

Rozwiązanie jest proste — użyj tylko funkcji z Win32 API do operacji na plikach.

Dokładnie tak jak piszesz, czyli zacznij od OpenFile lub CreateFile, a następnie korzystaj z ReadFile lub ReadFileEx do odczytu pakietów danych oraz WriteFile lub WriteFileEx. Do zamknięcia użyj CloseHandle, tak jak dokumentacja nakazuje.

0

Dziękuję wszystkim, a szczególnie @furious programming za odpowiedzi. Na razie wyjeżdżam na kilkudniową delegację, w weekend ponownie siądę do problemu i spróbuję skorzystać z Waszych sugestii.

0

Dodam jeszcze, że jeśli chodzi o takie funkcje jak Assign, Reset, Rewrite czy Close, to są to tylko i wyłącznie wrappery na wywołania systemowych funkcji, natomiast zmienna typu File jest nieprzezroczystą strukturą typu FileRec. W tej strukturze znajduje się pole Handle, posiadające zwyczajny uchwyt, rozumiany przez funkcje systemowe. I tak np. funkcja Reset otwiera plik za pomocą funkcji CreateFileW, a zwrócony przez nią uchwyt ląduje w polu Handle.

Tak więc jeśli bardzo chcesz używać pascalowych funkcji do operacji na plikach, to możesz z nich spokojnie korzystać, natomiast jeśli w którymś momencie będziesz chciał skorzystać z funkcji systemowej, możesz pozyskać z rozumiany przez system uchwyt za pomocą rzutowania zmiennej na typ FileRec:

var
  Source: File;
  SourceHandle: THandle;
begin
  Assign(Source, 'data.bin');
  Reset(Source);

  SourceHandle := FileRec(Source).Handle; // uchwyt dla funkcji z Win32 API.

To samo robi wbudowana funkcja do_open (w pliku sysfile.inc, linia 202), która bezpośrednio odpowiada za otwarcie pliku. Robi wiele rzeczy, ustawia flagi, atrybuty itd, a finalnie woła CreateFileW i wpisuje uchwyt do pola struktury (linia 300 w rzeczonym pliku):

filerec(f).handle:=CreateFileW(p,oflags,shflags,@security,cd,FILE_ATTRIBUTE_NORMAL,0);
0

Już wróciłem z delegacji i zrobiłem parę testów. Długo kombinowałem z programem. Czasami data ostatniego dostępu się zmieniała, a czasami nie. Przy czym data ostatniego dostępu wskazywana przez Windows (po kliknięciu na pliku prawym klawiszem i wyświetleniu jego właściwości) czasami się nie pokrywa z datą otrzymaną z funkcji GetFileTime.

W końcu coś się wyjaśniło. Zauważyłem dziwną rzecz: Wyświetlenie właściwości pliku w Windows zmienia czas ostatniego dostępu (odczytywany przez GetFileTime). Ale mimo to czas ostatniego dostępu wyświetlany przez Windows się nie zmienia.

Prawdopodobnie mój program już jest w tym zakresie dobry (to znaczy nie zmienia daty ostatniego dostępu), a te zmiany, które obserwowałem wynikają z tego, że klikałem na pliku prawym klawiszem i wyświetlałem jego właściwości.

Wygląda to tak, jakby Windows miał dwa miejsca, gdzie czas ostatniego dostępu jest przechowywany. Spotkaliście się może z tym problemem? Wiecie może, jak można odczytać lub zmodyfikować ten czas wyświetlany przez Windows?

0
skrzat napisał(a):

@tomac nie lepiej czas przeznaczyć na coś innego i skorzystać z gotowych rozwiązań? Są darmowe programy/systemy i umożliwiają tworzenie kopii różnicowej na poziomie fragmentów plików - gdy masz duży plik, i coś w nim zmieni się, to następna kopia nie jest całego pliku, tylko fragmentu obejmującego zmiany.
Ja używam od lat Ferro Backup System (kopia 2 komputerów za darmo) i jestem b.zadowolony.

Próbowałem znaleźć coś gotowego, ale może za słabo szukałem. A próbę napisania własnego programu traktuję po części jako próbę ponownego wejścia w świat programowania komputerów PC. Może mi się spodoba.

Tym niemniej jestem otwarty na gotowe programy. Rzuciłem okiem na polecany przez Ciebie Ferro Backup System, ale o ile dobrze rozumiem, wymaga co najmniej dwóch komputerów (jeden klient i jeden serwer). Mi natomiast chodzi o backupowanie na podłączalnych dyskach USB. Mam dwa dyski USB po 4TB i chciałbym na przemian robić backup raz na jednym, raz na drugim dysku.

tumor napisał(a):

A tak można zmodyfikować timestamp pliku za pomocą winapi:
GetLastAccessTime(String)
GetFileTime
SetFileTime
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfiletime

Możesz ustawić poprzedni timestamp, ale wcześniej pobierasz aktualny timestamp.

Problemem jest, jeśli czytany plik jest tylko do odczytu. W zasadzie można by kasować atrybut readonly, zmieniać datę, po czym ponownie ustawiać readonly. Nie testowałem tego, ale obawiam się, że zmiana atrybutu też zmieni czas ostatniego dostępu. Poza tym jest jeszcze inna kwestia: Pliki znajdujące się w lokalizacji, z której można tylko czytać (w folderach systemowych). Tu zmiana atrybutu readonly jest bardziej skomplikowana.

W zasadzie mam jeszcze inny pomysł: Zmienić czas systemowy na taki, jaki powinien mieć plik, w tym momencie odczytać plik, po czym przywrócić czas systemowy (oczywiście żartuję).

1 użytkowników online, w tym zalogowanych: 0, gości: 1