unit File_path;
interface

uses	SysUtils, Classes
		{$IFDEF LINUX}
			, dateutils, lclproc, lazfileutils, fileutil  // for MoveToTrash 
		{$ELSE} 
			, Windows
		{$ENDIF};
		
procedure do_Copy_file(const X_Source, X_Dest: String);
{$IFDEF LINUX}
function MoveToTrash(FileOrDirName:AnsiString): boolean;
{$ENDIF}
		
implementation

{$IFDEF LINUX}
 	{$IFDEF fileutil_bug_fixed_in_FPC_LAZARUS}
	{$ELSE}
		{$DEFINE COPY_MANUALLY}
	{$ENDIF}
{$ENDIF}

{$IFDEF FPC}
		{$DEFINE COPY_MANUALLY}
{$ENDIF}

procedure do_Copy_file(const X_Source, X_Dest: String);
{$IFDEF COPY_MANUALLY} 
	var ss, ds: TFileStream;
	{$IFDEF WINDOWS}
	var ft: TFileTime;
	{$ENDIF}
{$ENDIF}
begin
	{$IFDEF COPY_MANUALLY}
 		ss := TFileStream.Create(X_Source, fmOpenRead);
 		ds := TFileStream.Create(X_Dest, fmCreate);
 		try
 			{$IFDEF WINDOWS}
 			GetFileTime(ss.Handle, nil, @ft, nil);
 			{$ENDIF}
 			ds.CopyFrom(ss, ss.Size);
 			{$IFDEF WINDOWS}
			SetFileTime(ds.Handle, nil, @ft, nil);
			{$ENDIF}
 		finally
 			ss.Free;
 			ds.Free;
 		end;
	{$ELSE}
		{$IFDEF LINUX}
	 		CopyFile(X_Source, X_Dest);
		{$ELSE}
	 		CopyFile(PChar(X_Source), PChar(X_Dest), FALSE);
	 	{$ENDIF}
	{$ENDIF}
end; 	

(*
/*

CLASS {File_path|_for_Delphi_VCL_and_FPC},
NATIVE, STOPPING, TESTED(40), RECURSIVE,
ABSTRACT :

IMPORT: {System_functions}, {Date_time}, {String_functions} .
IMPORT, TEST: {Text_file} .

IMPORT, DEBUG: {Mathematics} .

NATIVE: @
		"uses File_path, System_functions"
		"{$IFDEF LINUX} "
		"	{$IFDEF fileutil_bug_fixed_in_FPC_LAZARUS}"
		"		, fileutil "
		"	{$ENDIF not_fixed}"
		"	, BaseUnix, Unix"
		"{$ELSE} "
		"		, Windows, ShellApi"
		"{$ENDIF};" .
		
NATIVE: "///COPY File_path.pas" .

        FUNCTION Exists|_file(STR Path|_of_file) ==> BOOL, NATIVE :
	        CASE Path == "" ? ==> ;
	        NATIVE     
           @"var sr: TSearchRec;"
           	"var e: DWORD;"
            "Result := FALSE;"
            "{$IFDEF LINUX}"
           	"   Result := FileExists({$IFDEF FPC}Str_X_Path"
           	"						 {$ELSE}X_Path{$ENDIF})"
           	" 		and not DirectoryExists({$IFDEF FPC}Str_X_Path"
           	"								{$ELSE}X_Path{$ENDIF});"
            "{$ELSE}"
            "	e := SetErrorMode(SEM_NOOPENFILEERRORBOX or "
            "			SEM_FAILCRITICALERRORS);"
            "	if SysUtils.FindFirst({$IFDEF FPC}Str_X_Path"
            "						  {$ELSE}X_Path{$ENDIF},"
            " 	   faAnyFile, sr) = 0 then"
            "	begin"
            "		if (sr.Attr and faDirectory = 0) "
            "		   {$IFDEF LINUX} and (sr.Name <> '') {$ENDIF} then"
            "		begin"
            "			Result := TRUE;"
            "		end;"
            "		SysUtils.FindClose(sr);"
            "	end;"
            "{$ENDIF}"
            "{$IFDEF LINUX}"
            "{$ELSE}"
            "SetErrorMode( e );"
            "{$ENDIF}" .

        FUN Delete_file(STR Path|_file) :
            CASE !Path.Exists && !Path.Dir_exists ? ==> ;
            Stop_if_test(Path)
            Path.delete .

        FUN delete|_file(STR Path|_file), NATIVE :
           @"try"
           	"	if DirectoryExists({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF})"
           	"	then"
           	"		RemoveDir({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF})"
           	"	else"
            "   	SysUtils.DeleteFile("
            "			{$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF});"
            "except on E: Exception do"
            "   begin"
            "       Any_exception(E);"
            "   end"
            "end;" .
            
        FUN Can_delete_dir_not_empty ==> BOOL :
            = FALSE .

        FUN Recycle_file|s_semicolon_separated(
        STR Path|_files)
        	:
            Stop_if_test(Path)
            STR all|_files[].Split(Path, ";")
            FOR a IN all[] :
                all[a.Index] = a.Remove_trailing(Path_sep) ;
            all[].recycle .

        FUN Remove_file|s_semicolon_separated(
        STR Path|_files)
        	:
            Stop_if_test(Path)
            STR all|_files[].Split(Path, ";")
            FOR a IN all[] :
                all[a.Index] = a.Remove_trailing(Path_sep) ;
            all[].rem_files .

        FUN recycle|_file(STR Paths|_files[]), NATIVE :
        @
       "var L : Integer; {$IFDEF LINUX}"
       "	{$ELSE} FOS : TSHFileOpStructW;"
       "			W: WideString;"
       "			bufW: PWideChar;"
       "	{$ENDIF}"
       "var i: Integer;"
       "var ToList0: String;"
       "{$IFDEF LINUX}"
       "	for i := 0 to X_Paths.Count()-1 do"
       "	begin"
       "		MoveToTrash( X_Paths.GetStrings(i));"
       "	end;"
       "{$ELSE}"
       "	ToList0 := '';"
       "	for i := 0 to X_Paths.Count()-1 do"
       "		ToList0 := ToList0 + X_Paths.GetStrings(i) + #0;"
       "	L := Length( ToList0 );"
       "	FillChar(FOS, Sizeof(FOS), 0);"
       "	FOS.wFunc := FO_DELETE;"
       "	W := ToList0;"
       "	GetMem(bufW, (Length(W)+2) * Sizeof(WideChar));"
       "	FillChar(bufW[0], (Length(W)+2)*Sizeof(WideChar), 0);"
       "	move(W[1], bufW[0], Length(W) * Sizeof(WideChar));"
       "	FOS.pFrom := @bufW[0];"
       "	FOS.pTo := '';"
       "	FOS.fFlags := FOF_ALLOWUNDO or FOF_NOCONFIRMATION or 1556;"
       "													  // FOF_NO_UI;"
       "	{$IFDEF FPC} SHFileOperationW( @FOS )"
       " 	{$ELSE} SHFileOperationW( FOS )"
       "	{$ENDIF};"
       "	FreeMem(bufW);"     
       "{$ENDIF}" .
       
        FUN rem_files(STR Paths|_files[]), NATIVE :
        @
       "var L : Integer; {$IFDEF LINUX}"
       "		p1: String;"     
       "	{$ELSE} FOS : TSHFileOpStructW;"
       "			W: WideString;"
       "			bufW: PWideChar;"
       "	{$ENDIF}"
       "var i: Integer;"
       "var ToList0: String;"
       "{$IFDEF LINUX}"
       "    try"
       "		for i := 0 to X_Paths.Count()-1 do"
       "		begin"    
       "			p1 := X_Paths.GetStrings(i);"     
       "            if DirectoryExists(p1)"
       "            then"
       "               RemoveDir(p1)"
       "            else"
       "               	SysUtils.DeleteFile(p1);"
       "		end;"     
       "    except on E: Exception do"
       "       begin"
       "           Any_exception(E);"
       "       end"
       "    end;"
       "{$ELSE}"
       "	ToList0 := '';"
       "	for i := 0 to X_Paths.Count()-1 do"
       "		ToList0 := ToList0 + X_Paths.GetStrings(i) + #0;"
       "	L := Length( ToList0 );"
       "	FillChar(FOS, Sizeof(FOS), 0);"
       "	FOS.wFunc := FO_DELETE;"
       "	W := ToList0;"
       "	GetMem(bufW, (Length(W)+2) * Sizeof(WideChar));"
       "	FillChar(bufW[0], (Length(W)+2)*Sizeof(WideChar), 0);"
       "	move(W[1], bufW[0], Length(W) * Sizeof(WideChar));"
       "	FOS.pFrom := @bufW[0];"
       "	FOS.pTo := '';"
       "	FOS.fFlags := FOF_NOCONFIRMATION or 1556;"
       " 									  //FOF_NO_UI;"
       "	{$IFDEF FPC} SHFileOperationW( @FOS )"
       " 	{$ELSE} SHFileOperationW( FOS )"
       "	{$ENDIF};"
       "	FreeMem(bufW);"     
       "{$ENDIF}" .
       
*/
*)

{$IFDEF LINUX}
function MoveToTrash(FileOrDirName:AnsiString): boolean;
var
  TrashCanBase  : string = '~/.local/share/Trash/';
  TrashCanFiles : string = '~/.local/share/Trash/files/';
  TrashCanInfo  : string = '~/.local/share/Trash/info/';
  TrashCanAlt   : string;
  InfoFile      : TStringList;
  isDirectory               : boolean;
  newFileOrDirName          : string;
  NamePart1, NamePart2      : string;
  FilesInDirectory          : TStringList;
  counter                   : integer;

  function EscapeURLString(s: string): string;
  var
    i: integer;
    source: PAnsiChar;
  begin
    Result := '';
    source := pansichar(s);
    for i := 1 to length(source) do
      if not (source[i - 1] in ['A'..'Z', 'a'..'z', '0'..'9', '-', '_', '~', '.', ':', '/']) then
        Result := Result + '%' + IntToHex(Ord(source[i - 1]), 2)
      else
        Result := Result + source[i - 1];
  end;

begin
  if RightStr(FileOrDirName,1)=DirectorySeparator then  // remove directory separator if this is the last char
    FileOrDirName := LeftStr(FileOrDirName,Length(FileOrDirName)-1);

  // Are we moving a directory into the trashcan?
  isDirectory   := DirectoryExists(FileOrDirName);

  TrashCanFiles := ExpandFileName(TrashCanFiles);
  TrashCanInfo  := ExpandFileName(TrashCanInfo);
  TrashCanBase  := ExpandFileName(TrashCanBase);

  if not DirectoryExists(TrashCanBase) then  // We didn't find the standard dir, so now we are guessing alternatives
    begin
      TrashCanAlt := '';

      if DirectoryExists(ExpandFileName('~/Desktop/Trash/')) then
        TrashCanAlt:=ExpandFileName('~/Desktop/Trash/')
      else if DirectoryExists(ExpandFileName('~/trash/')) then
        TrashCanAlt:=ExpandFileName('~/trash/')
      else if DirectoryExists(ExpandFileName('~/Trash/')) then
        TrashCanAlt:=ExpandFileName('~/Trash/')
      else if DirectoryExists(ExpandFileName('~/.trash/')) then
        TrashCanAlt:=ExpandFileName('~/.trash/')
      else if DirectoryExists(ExpandFileName('~/.Trash/')) then
        TrashCanAlt:=ExpandFileName('~/.Trash/');

      if TrashCanAlt='' then  // we didn't find an alternative: create standard dirs
        begin
           ForceDirectories(TrashCanFiles);
           ForceDirectories(TrashCanInfo);
        end
      else                   // we did find an alternative, make sure we use it for files (and not for trashinfo)
        begin
          TrashCanFiles := TrashCanAlt;
          TrashCanInfo  := '';
        end;
    end;

  newFileOrDirName := ExtractFileName(FileOrDirName);
  //NamePart1 := lazfileutils.ExtractFileNameWithoutExt(newFileOrDirName);
  NamePart2 := ExtractFileExt(newFileOrDirName);
  NamePart1 := ChangeFileExt(ExtractFileName(newFileOrDirName), '');
  counter   := 0;

  // if file already in Trashcan, we add a number (eg. example.doc, example.2.doc, expample.3.doc etc.)
  while (isDirectory      and DirectoryExists(TrashCanFiles+newFileOrDirName)) or
        (not(isDirectory) and FileExists(TrashCanFiles+newFileOrDirName)) do
    begin
      inc(Counter);
      newFileOrDirName := NamePart1 + BoolToStr(Counter>1,'.'+IntToStr(Counter),'') + NamePart2;
    end;

  // Create a trashinfo file, if the standard dir did exist!
  if TrashCanInfo<>'' then
    begin
      InfoFile := TStringList.Create;
      InfoFile.Text := '[Trash Info]'+LineEnding+
                       'Path='+EscapeURLString(FileOrDirName)+LineEnding+
                       'DeletionDate='+FormatDateTime('YYYY-MM-DD',Now)+'T'+FormatDateTime('hh:nn:ss',Now)+LineEnding; //'2020-01-09T17:05:41'+
      InfoFile.SaveToFile(TrashCanInfo+newFileOrDirName+'.trashinfo');
      InfoFile.Free;
    end;

  // make new filename now full path
  newFileOrDirName := TrashCanFiles+newFileOrDirName;

  // Move File or Directory - try rename first, if that fails try copying files, and if copying worked delete originals
  Result := RenameFile(FileOrDirName,newFileOrDirName);  // try moving file or dir

  if not Result then // if rename failed then try copy and delete (aka move)
    begin
      if isDirectory then
        begin
          FilesInDirectory := FindAllFiles(FileOrDirName, '*', true);

          for Counter:=0 to FilesInDirectory.Count-1 do  // Copy the file structure to the trashcan
            begin
              if ForceDirectories( ExtractFilePath( StringReplace(FilesInDirectory.Strings[Counter],FileOrDirName,newFileOrDirName,[] ) ) ) then
                begin
                  if not CopyFile( FilesInDirectory.Strings[Counter], StringReplace(FilesInDirectory.Strings[Counter],FileOrDirName,newFileOrDirName,[] ) ) then
                    begin  // failed copying file - cleanup and bail
                      Result := false;

                      if DeleteDirectory(newFileOrDirName,true) then // delete what we have copied so far - things are going side ways
                        RemoveDir(newFileOrDirName);

                      if FileExists(TrashCanInfo+newFileOrDirName+'.trashinfo') then
                        DeleteFile(TrashCanInfo+newFileOrDirName+'.trashinfo');

                      Exit;
                    end;
                end
              else // failed creating dir - cleanup and bail
                begin
                  Result := false;

                  if DeleteDirectory(newFileOrDirName,true) then // delete what we have copied so far - things are going sideways
                    RemoveDir(newFileOrDirName);

                      if FileExists(TrashCanInfo+newFileOrDirName+'.trashinfo') then
                        DeleteFile(TrashCanInfo+newFileOrDirName+'.trashinfo');

                  Exit;
                end;
            end;

          // all went well, remove old dir
          if DeleteDirectory(FileOrDirName,true) then
            Result := RemoveDir(FileOrDirName);

          FilesInDirectory.Free;
        end
      else
        begin
          if CopyFile(FileOrDirName,newFileOrDirName,true,false) then         // If rename fails: copy file
            Result := DeleteFile(FileOrDirName)                               // If copy succeeded: remove original
          else
            Result := false;                                                  // Copy/Delete failed d
        end;
    end;
end;
{$ENDIF}

(*
/*

        FUN Rename|_file(STR Path|_file, STR New|_name) :
            CASE !Path.Exists && !Path.Dir_exists ? ==> ;
            Stop_if_test(Path)
            Stop_if_test(New)
            rename(Path.Remove_trailing(Path_sep), New.Remove_trailing(Path_sep)) .

        FUN rename|_file(STR Path|_file, STR New|_name), NATIVE :
        	BOOL only_reg|istry_of_letters_changing = Path.Lower == New.Lower
        	NATIVE @
        "try"
            //"	 writeln('try rename[' + X_Path']-->[' + X_New + ']');"
            "    if not FileExists({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF})"
            "	 and not DirectoryExists("
            "			 {$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF})"     
            "	 then "
            "	 begin"
            //"		writeln('---------- file not exists:' + X_Path);"
            "		Exit;"
            "	 end;"
            "    if not X_only_reg "
            "		and FileExists({$IFDEF FPC}Str_X_New{$ELSE}X_New{$ENDIF}) "
            "	 then"
            "		SysUtils.DeleteFile("
            "			{$IFDEF FPC}Str_X_New{$ELSE}X_New{$ENDIF});"
            "    if FileExists({$IFDEF FPC}Str_X_Path"
            "				   {$ELSE}X_Path{$ENDIF}) "
            "	 or DirectoryExists({$IFDEF FPC}Str_X_Path"
            "				   {$ELSE}X_Path{$ENDIF}) then"
            "		RenameFile({$IFDEF FPC}Str_X_Path"
            "				   {$ELSE}X_Path{$ENDIF},"
            "				   {$IFDEF FPC}Str_X_New{$ELSE}X_New{$ENDIF})"
            //"	 else writeln('---------- file not exists: ' + X_Path)"
            "	 ;"
            "except on E: Exception do"
            "   begin"
            //"		writeln('*********** renaming exception:' + E.Message);"
            "       Any_exception(E);"
            "   end"
            "end;"
        .

        FUN Move_file(STR Path|_file, STR New|_path) :
            Rename(Path, New) .

        FUN Move_file_direct|ly_without_checks(
        STR Path|_file, STR New|_path), NATIVE : @
        "try"
            "	 RenameFile({$IFDEF FPC}Str_X_Path"
            "				{$ELSE}X_Path{$ENDIF},"
            "				{$IFDEF FPC}Str_X_New{$ELSE}X_New{$ENDIF})"
            "	 ;"
        "except"
        "end;" .

        FUN Copy_file(STR S|ource_file_path, STR D|estination) :
            CASE !S.Exists ? ==> ;
            CASE !D.Extract_path.Directory_exists ? ==> ;
            Stop_if_test(S)
            Stop_if_test(D)
            do_copy_file(S, D) .

        FUN do_copy_file(STR S|ource_file_path, STR D|estination),
                NATIVE :
        @	"do_Copy_file(X_S, X_D);" .

        TEST file_manipulations :
             ---------------------------------------------- 'Fake file'
             STR fake|_path = "<?>"
             CASE !fake.Exists ?
                  fake.Delete_file
                  Rename_file(fake, "??");
             fake = Start_dir.Trailing_slash "Test".Trailing_slash "My_file.txt"
             CASE fake.Exists ? fake.Delete_file ;
             text|_file_temp = {Text_file}(Path = fake)
             STR lines|_to_save[] << "line1" << "line2" << "line3"
             CASE !fake.Exists ?
                  ASSERT text.Save(lines[])
                  ASSERT fake.Exists
                  fake.Delete_file
                  ASSERT !fake.Exists
                  ------------------------------------ 'rename'
                  ASSERT text.Save(lines[])
                  ASSERT fake.Exists
                  
                  STR to|_what_rename = Start_dir.Trailing_slash
                      "Test".Trailing_slash "My_test_file2.txt"
                      
                  CASE to.Exists ?
                       Delete_file(to)
                       ASSERT !to.Exists ;
                  Rename(fake, to)
                  ASSERT to.Exists
                  ------------------------------------ 'move'
                  ASSERT text.Save(lines[])
                  ASSERT fake.Exists
                  
                  to = Start_dir.Trailing_slash
                      "Test".Trailing_slash "My_test_file2.txt"
                      
                  //STR in|put_test_str
                  //<< "moving to: " to >> in
                  CASE to.Exists ?
                       Delete_file(to)
                       ASSERT !to.Exists ;
                  Move_file(fake, to)
                  ASSERT to.Exists ;
                  ------------------------------------ 'copy'
                  ASSERT text.Save(lines[])
                  ASSERT fake.Exists
                  
                  to = Start_dir.Trailing_slash
                      "Test".Trailing_slash "My_test_file3.txt"

                  CASE to.Exists ?
                       Delete_file(to)
                       ASSERT !to.Exists ;
                  Copy_file(fake, to)
                  ASSERT to.Exists
                  ------------------------------------- 'copy again'
                  CASE (fake "-- not existing").Exists ?
                       Delete_file(fake "-- not existing") ;
                  Copy_file(fake "-- not existing", to)
                  Copy_file(fake, to)
                  Copy_file(fake, to "\#:a\#")
                  ASSERT to.Exists
             .

        FUNCTION Directory_exists || Dir_exists(
        STR Path|_of_dir) ==> BOOL, NATIVE :
			STR path_bs|_with_slash = Path.Trailing_slash
			NATIVE @
			"var e: DWORD;"
			"{$IFDEF LINUX}"
			"{$ELSE}"
			"	 e := SetErrorMode( "
			"			SEM_NOOPENFILEERRORBOX or SEM_FAILCRITICALERRORS );"
			"{$ENDIF}"
            "    Result := DirectoryExists("
            "			X_path_bs{$IFDEF FPC}.As_string{$ENDIF});"
            "{$IFDEF LINUX}"
            "{$ELSE}"
			"	 SetErrorMode( e ); "
			"{$ENDIF}" .

        FUNCTION Create_dir|ectory(
        STR Path|_to_new_dir), RECURSIVE
        	:
			CASE Path_sep IN Path || ":" IN Path ?
				 STR p|arent_directory = Path.Remove_trailing(
					 "\").Remove_trailing("/").Extract_path
				 CASE !p.Directory_exists && !p.Ending(":") ?
				      p.Create_dir ;
			;
			CASE Path.Directory_exists ? ==> ;
            //DEBUG:
            //      << #NL"try create dir: [" Path "]" #NL#NL ; 
			do_Create_dir(Path) .
        FUN do_Create_dir(STR Path|_to_new_dir), NATIVE : @
            "    MkDir({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF});" .

        FUN Catalog|ue_of_directory(STR Dir|ectory_path,
                STR Cat|alogue_scanned[]), NATIVE :
			STR fdir|ectory_fixed = Dir.Trailing_slash
			NATIVE
		@	"var sr: TSearchRec;"
            "var path: String;"
            "    X_Cat.Clear;"
            "    path := X_fdir;"
            "    if not DirectoryExists(path) then Exit;"
            "    if SysUtils.FindFirst(path + '*', faAnyFile, sr) = 0 then"
            "    REPEAT"
            "        if (sr.Name = '.') or (sr.Name = '..') then"
            "           continue;"
            "		 {$IFDEF LINUX}"
            "		     if (sr.Attr and faDIRECTORY) <> 0"
            "		     then X_Cat.Add(MAXINT, sr.Name + '/')"
            "            else X_Cat.Add(MAXINT, sr.Name);"
            "		 {$ELSE}"
            "		     if (sr.Attr and FILE_ATTRIBUTE_DIRECTORY) <> 0"
            "		     then X_Cat.Add(MAXINT, sr.Name + '\')"
            "            else X_Cat.Add(MAXINT, sr.Name);"
            "		 {$ENDIF}"
            "    UNTIL SysUtils.FindNext(sr) <> 0;"
            "    SysUtils.FindClose(sr);" .

        FUN Get_cat|alog|ue|_of_path(
        STR Cat|alogue_scanned[], STR Path|_of_directory)
        :
		Catalog(Path, Cat[]) .
            
        //ENUM {file_attr|ibutes} :
        //    'READONLY   |_FILE',
        //    'HIDDEN     |_FILE_DIRECTORY',
        //    'SYSTEM     |_FILE_DIRECTORY',
        //    'DIRECTORY  |_NOT_A_FILE',
        //    'ARCHIVE    |_FILE',
        //    'NORMAL     |_FILE',
        //    'TEMPORARY  |_FILE',
        //    'COMPRESSED |_FILE',
        //    'OFFLINE    |_FILE',
        //    'RESERVE_1',
        //    'RESERVE_2' .
            
        CONST INT {file_attr|ibutes} : 
	         READ_ONLY		= 1 
	         HIDDEN|_FILE	= 2
	         SYSTEM|_FILE	= 4
	         DIRECTORY		= 16
	         ARCHIVE|_ATTR	= 32
	         NORMAL |_ATTR	= 128
	         TEMPORARY		= 256
	         COMPRESSED		= 0x800
	         OFFLINE|_ATTR	= 0x1000
	         RESERVE_1		= 8
	         RESERVE_2		= 64
	         .

        STRUCTURE {file_detail|ed_info} :
            INT Attrib|utes_file
            REAL Size|_in_bytes
            REAL Creation|_date_time
            REAL Modified|_date_time
            REAL Accessed|_date_time .


        FUN Catalog_detailed( STR Dir|ectory_path,
            STR Cat|alogue_scanned[], {file_detail} Det|ails_for_files[])
            :
			Cat[].Clear
			Det[].Clear
			Catalog(Dir, Cat[])
			STR path|_trailed_slash = Dir.Trailing_slash
			--------------------- 'getting details on each file found'
			FOR s IN Cat[] :
				Det[] << File_detail(path s.Remove_trailing(Path_separator))
			; .
			
				
        FUNCTION File_detail|ed_info(
        STR Path|_to_file ) ==> {file_detail}
        	:
            CASE Path == "" ? ==> ;
            INT attr|ibutes[]
            REAL times|_of_file[]
            
            RESULT.Size = native_File_details(Path.Remove_trailing(Path_sep),
            	attr[], times[])
            -------------------------------- 'times'
            RESULT.Creation = times[0]
            RESULT.Modified = times[1]
            RESULT.Accessed = times[2]
            -------------------------------- 'attribs'
            RESULT.Attrib = attr[0]
            .
        FUN native_File_details(STR Path|_of_file,
        	INT attr|ibutes_put_in_0[], REAL times|_of_file[]) ==> REAL,
            NATIVE :
            STR filename = Path.Extract_file_name
            NATIVE @
        @   "var sr: TSearchRec;"
            "var fa: Integer;"
            "var ST: TSystemTime;"
            "{$IFDEF LINUX}"
            "{$ELSE}"     
            "var LT: TFileTime;"
            "{$ENDIF}"     
            "var a: Integer;"
            "var q: Double;"
            "	 q := 24.0 * 3600000;"
            "if SysUtils.FindFirst({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF},"
            "	faAnyFile, sr) = 0 then"
            "begin"
            "    fa := sr.Attr;"
            "	 a := 0;"
            "	{$IFDEF LINUX}"
            "    if fa and faREADONLY <> 0 then a := a or 1;"
            "    if (fa and faHIDDEN <> 0) or (X_filename.CharAt(0)='.') then"
            " 		a := a or 2;"
            "    if fa and faSysFile <> 0 then a := a or 4;"
            "    if fa and faDIRECTORY <> 0 then a := a or 16;"
            "    if fa and faARCHIVE <> 0 then a := a or 32;"
            "	 X_attr.Add(-1, a);"
            "    Result := sr.Size;"
            "    X_times.Add(-1, FileDateToDateTime(sr.Time) * q);"
            "    X_times.Add(-1, FileDateToDateTime(sr.Time) * q);"
            "    X_times.Add(-1, FileDateToDateTime(sr.Time) * q);"
            "	{$ELSE}"
            "    a := fa;"
            "	 X_attr.Add(-1, a);"
            "	 RESULT := sr.FindData.nFileSizeLow +"
            "		sr.FindData.nFileSizeHigh * 4294967296.0;"     
            "    FileTimeToLocalFileTime(sr.FindData.ftCreationTime, LT);"
            "    FileTimeToSystemTime(LT, ST);"
            "    X_times.Add(-1, SystemTimeToDateTime(ST) * q);"
            "    FileTimeToLocalFileTime(sr.FindData.ftLastWriteTime, LT);"
            "    FileTimeToSystemTime(LT, ST);"
            "    X_times.Add(-1, SystemTimeToDateTime(ST) * q);"
            "    FileTimeToLocalFileTime(sr.FindData.ftLastAccessTime, LT);"
            "    FileTimeToSystemTime(LT, ST);"
            "    X_times.Add(-1, SystemTimeToDateTime(ST) * q);"
            "	{$ENDIF}"
            "    SysUtils.FindClose(sr);"
            "end;"
            .
            
        FUN File_size_kbytes(STR File_name) ==> REAL :
        	RESULT = File_detail(File_name).Size / 1024 .
            
            
        FUNCTION Change_ext|ension(STR Path|_of_file,
            STR New_ex|tension) ==> STR
            :
            INT dot|_position = Path.Find_last(".")
            CASE dot < 0 ? dot = Path.Len ;
            RESULT = Path.[0 TO dot-1] New_ex .
            
        FUN Ext|ension|_of_file(STR Path|_of_file) ==> STR :
        	STR no_ext|ension = Path.Change_ext("")
        	RESULT = Path.[no_ext.Len TO *] .

        TEST changing_extension:
             ASSERT Change_ext("x.xyz", ".abc") == "x.abc"
             ASSERT Change_ext("x/", ".abc") == "x/.abc"
             ASSERT Change_ext("x", ".abc") == "x.abc" .

        FUNCTION Extract_path( STR Path|_of_file_full) ==> STR :
            FOR i IN [Path.Len-1 DOWNTO 0] :
                CASE Path.[i] IN ":\/" ? RESULT = Path.[0 TO i] ==> ;
            ; .

        FUNCTION Extract_file_name( STR Path|_file) ==> STR :
            FOR i IN [Path.Len-1 DOWNTO 0] :
                CASE Path.[i] IN ":\/" ? RESULT = Path.[i+1 TO *] ==> ;
            ;
            RESULT = Path .

        TEST of_extract :
            ASSERT Extract_file_name("path\a") == "a"
            ASSERT Extract_file_name("a") == "a" .
            
		FUNCTION Get_file_size(
		STR Path|_file) ==> REAL 
			:
			{file_detail} fd|etail_on_file_temp = File_detail(Path)
			RESULT = fd.Size .

		FUNCTION Get_file_time_age_seconds(
		STR Path|_file) ==> REAL, 
		DEPRECATED('use only for compatibility with the primary AL4 compiler')
			:
			{file_detail} fd|etail_on_file_temp = File_detail(Path)
			RESULT = Now.Seconds_diff(Date_time(DT=fd.Modified)) .
			
		FUNCTION Get_file_modified_time_formatted(
		STR Path|_file) ==> STR,
		DEPRECATED('use only for compatibility with the primary AL4 compiler')
		:
		CASE Path == "" || (!Path.Exists && !Path.Dir_exists) ? ==> ;
		{file_detail} fd|etail_on_file_temp = File_detail(Path)
		RESULT = Date_time(DT=fd.Modified).Format("[YYYYMMDD HHmmss]") .

		FUN Set_file_time_age(
		STR Path|_file, {Date_time}.{date_time} Age|_2_seconds_recision),
		DEPRECATED('Platform dependant')
			:
			do_Set_file_time_age(Path, Age.DT) .
	 	FUN do_Set_file_time_age(
	 	STR Path|_file, REAL Age|_2_seconds_recision), NATIVE :
			CASE !Path.Exists ? ==> ;
		NATIVE @
			"var st: TSystemTime; "
			"var hFile: THandle; {$IFDEF WINDOWS} ft: TFileTime; {$ENDIF}"
			"{$IFDEF WINDOWS}"
			"hFile := CreateFile(PChar("
			"	{$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF}),"
			"	GENERIC_READ or GENERIC_WRITE,"
			"	FILE_SHARE_WRITE or FILE_SHARE_READ, nil, OPEN_EXISTING, "
			"	FILE_ATTRIBUTE_SYSTEM or FILE_ATTRIBUTE_HIDDEN, 0);"
			"if hFile = INVALID_HANDLE_VALUE then Exit;"
			"DateTimeToSystemTime(X_Age/24.0/3600000,st);"
			"SystemTimeToFileTime(st, ft);"
			"SetFileTime(hFile, nil, nil, @ft);"
			"CloseHandle(hFile);"
			"{$ENDIF}" 
			.
			
		FUN Set_file_creation_time(
		STR Path|_file,
		{Date_time}.{date_time} CT | ime_2_seconds_recision),
		DEPRECATED('Platform dependant') 
			:
			do_Set_file_creation_time(Path, CT.DT) .
	 	FUN do_Set_file_creation_time(
	 	STR Path|_file, REAL CT| ime_2_seconds_recision), NATIVE:
			CASE !Path.Exists ? ==> ;
		NATIVE @
			"var st: TSystemTime; "
			"var hFile: THandle; {$IFDEF WINDOWS} ft: TFileTime; {$ENDIF}"
			"{$IFDEF WINDOWS}"
			"hFile := CreateFile(PChar("
			"	{$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF}),"
			"	GENERIC_READ or GENERIC_WRITE,"
			"	FILE_SHARE_WRITE or FILE_SHARE_READ, nil, OPEN_EXISTING, "
			"	FILE_ATTRIBUTE_SYSTEM or FILE_ATTRIBUTE_HIDDEN, 0);"
			"if hFile = INVALID_HANDLE_VALUE then Exit;"
			"DateTimeToSystemTime(X_CT/24.0/3600000,st);"
			"SystemTimeToFileTime(st, ft);"
			"SetFileTime(hFile, @ft, nil, nil);"
			"CloseHandle(hFile);"
			"{$ENDIF}" 
			.
			
		FUN Set_file_last_access_time(
		STR Path|_file,
		{Date_time}.{date_time} CT | ime_2_seconds_recision),
		DEPRECATED('Platform dependant') 
			:
			do_Set_file_last_access_time(Path, CT.DT) .
	 	FUN do_Set_file_last_access_time(
	 	STR Path|_file, REAL CT| ime_2_seconds_recision), NATIVE:
			CASE !Path.Exists ? ==> ;
		NATIVE @
			"var st: TSystemTime; "
			"var hFile: THandle; {$IFDEF WINDOWS} ft: TFileTime; {$ENDIF}"
			"{$IFDEF WINDOWS}"
			"hFile := CreateFile(PChar("
			"	{$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF}),"
			"	GENERIC_READ or GENERIC_WRITE,"
			"	FILE_SHARE_WRITE or FILE_SHARE_READ, nil, OPEN_EXISTING, "
			"	FILE_ATTRIBUTE_SYSTEM or FILE_ATTRIBUTE_HIDDEN, 0);"
			"if hFile = INVALID_HANDLE_VALUE then Exit;"
			"DateTimeToSystemTime(X_CT/24.0/3600000,st);"
			"SystemTimeToFileTime(st, ft);"
			"SetFileTime(hFile, nil, nil, @ft);"
			"CloseHandle(hFile);"
			"{$ENDIF}" 
			.
			
		FUN Set_file_attr|ibutes(
		STR Path|_file, STR Attr|ibutes_Hidden_Readonly_System), NATIVE,
		DEPRECATED('Platform dependent') :
			CASE Path.[*]==Path_sep ?
			     CASE !Path.Dir_exists ? ==> ;
			ELSE
			     CASE !Path.Exists ? ==> ;
			;
			BOOL hidden|_file = "H" IN Attr
			BOOL readonly = "R" IN Attr
			BOOL system|_file = "S" IN Attr
			STR ugo|user_group_others[]
			Split(ugo[], Attr, "|")
		NATIVE @
		"var flags: {$IFDEF LINUX} TMode; {$ELSE} DWORD; {$ENDIF}"
		"{$IFDEF LINUX}"
		"	flags := 0;"
		"	if pos('R', X_ugo.GetStrings(0)) > 0 then flags := flags or S_IRUSR;"
		"	if pos('W', X_ugo.GetStrings(0)) > 0 then flags := flags or S_IWUSR;"
		"	if pos('X', X_ugo.GetStrings(0)) > 0 then flags := flags or S_IXUSR;"
		"	if pos('R', X_ugo.GetStrings(1)) > 0 then flags := flags or S_IRGRP;"
		"	if pos('W', X_ugo.GetStrings(1)) > 0 then flags := flags or S_IWGRP;"
		"	if pos('X', X_ugo.GetStrings(1)) > 0 then flags := flags or S_IXGRP;"
		"	if pos('R', X_ugo.GetStrings(2)) > 0 then flags := flags or S_IROTH;"
		"	if pos('W', X_ugo.GetStrings(2)) > 0 then flags := flags or S_IWOTH;"
		"	if pos('X', X_ugo.GetStrings(2)) > 0 then flags := flags or S_IXOTH;"
		"	FpChMod({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF}, flags);"
		"{$ELSE}"
		"	flags := FILE_ATTRIBUTE_NORMAL;"
		"	if (X_hidden) then flags := flags or FILE_ATTRIBUTE_HIDDEN;"
		"	if (X_readonly) then flags := flags or FILE_ATTRIBUTE_READONLY;"
		"	if (X_system) then flags := flags or FILE_ATTRIBUTE_SYSTEM;"
		"	SetFileAttributes(PChar("
		"		{$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF}), flags);"
		"{$ENDIF}" .
			
			
FUN Format_size(REAL Size|_in_bytes) ==> STR :
	CASE ?
	[Size < 1024] ? RESULT = Size.Int.S " Bytes"
	[Size < 1024 * 1024 * 0.9] ? RESULT = (Size/1024).S 
		----------------------------- 'rest 1 decimal digit'
		RESULT = RESULT.Replace(",", ".")
		CASE "." IN RESULT ? RESULT = RESULT.[0 TO RESULT.Find(".")+1] ;
		RESULT = _ " KBytes"
	[Size < 1024.0 * 1024 * 1024 * 0.9] ? RESULT = (Size/1024/1024).S
		----------------------------- 'rest 2 decimal digit', REUSED
		RESULT = RESULT.Replace(",", ".")
		CASE "." IN RESULT ? RESULT = RESULT.[0 TO RESULT.Find(".")+2] ;
		----------------------------- 'end'
		RESULT = _ " MBytes"
	[Size < 1024.0 * 1024 * 1024 * 1024 * 0.75] ?
		RESULT = (Size/1024/1024/1024).S
		LIKE ........................ 'rest 2 decimal digit'
		RESULT = _ " GBytes"
	[Size < 1024.0 * 1024 * 1024 * 1024 * 1024 * 0.75] ?
		RESULT = (Size/1024/1024/1024/1024).S
		LIKE ........................ 'rest 2 decimal digit'
		RESULT = _ " TBytes"
	[Size < 1024.0 * 1024 * 1024 * 1024 * 1024 * 1024 * 0.75] ?
		RESULT = (Size/1024/1024/1024/1024/1024).S
		LIKE ........................ 'rest 2 decimal digit'
		RESULT = _ " PBytes"
	ELSE
		RESULT = (Size/1024/1024/1024/1024/1024/1024).S
		LIKE ........................ 'rest 2 decimal digit'
		RESULT = _ " HBytes" ; .
		
TEST of_Format_size :
	ASSERT Format_size(999) == "999 Bytes", Format_size(999)
	ASSERT Format_size(1999) == "1.9 KBytes", Format_size(1999)
	ASSERT Format_size(2550) == "2.4 KBytes", Format_size(2550)
	ASSERT Format_size(3000_000.0) == "2.86 MBytes", Format_size(3000_000.0)
	----------------------------------- 'even bigger'
	ASSERT Format_size(4_000_000_000.0) == "3.72 GBytes",
		   Format_size(4_000_000_000.0)
		   
	ASSERT Format_size(5_000_000_000_000.0) == "4.54 TBytes",
		   Format_size(5_000_000_000_000.0)
		   
	ASSERT Format_size(6_000_000_000_000_000.0) == "5.32 PBytes",
		   Format_size(6_000_000_000_000_000.0)
		   
	ASSERT Format_size(7_000_000_000_000_000_000.0) == "6.07 HBytes",
		   Format_size(7_000_000_000_000_000_000.0) .
		   
FUN File_to_string | for_small_files_ONLY(
STR Path|_of_file) ==> STR, NATIVE :
	CASE !Path.Exists ? ==> ;
	NATIVE @
	"var f: TFileStream;"
	"	f := TFileStream.Create({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF},"
			"fmOpenRead or fmShareDenyNone);"
	"	try"
	"		if f.Size > 1000000 then Exit;"
	"		SetLength(RESULT, f.Size);"
	"		f.Read(RESULT[1], Length(RESULT));"
	"	finally"
	"		f.Free;" 
	"	end;" .
	
FUN Oem_file_to_string | for_small_files_ONLY(
STR Path|_of_file) ==> STR 
	:= File_to_string(Path) .
	
		   
FUN String_to_file(
STR S|tring_to_save, STR Path|_of_file), NATIVE :
	Stop_if_test(Path)
	NATIVE @
	"var f: TFileStream; mode: Integer;"
	"try"
    "	if FileExists({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF}) then"
    "		 mode := fmOpenWrite"
    "	else mode := fmCreate;"
	"	f := TFileStream.Create({$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF},"
	"		mode);"
	"	try"
	"		if {$IFDEF FPC}Str_X_Path{$ELSE}X_Path{$ENDIF} <> '' then begin"     
	"		   {$IFDEF FPC}"     
	"		   f.Write(Str_X_S[1], Length(Str_X_S));"
	"		   {$ELSE}"     
	"		   f.Write(X_S[1], Length(X_S));"
	"		   {$ENDIF}"     
	"		end;"     
	"	finally"
	"		f.Free;" 
	"	end;"
	"except"
	"end;" .

END
*/
*)
end.

