Why is this compare failing - delphi

I have the following function with parameters
aFile = a full filename
aFolder = a foldername to copy/move to
aGuid = the guid that the document is assigned
aAction = what to do with the fil (move or copy)
I would guess the line if Trim(NewFile) = Trim(aFile) then Exit should stop the code from doing anything if the old file is the same as the new. But it doesn't. The line if FileExists(NewFile) is executed even if the files are the same.
In my debug log I have
30-05-2013 08:10:34:840 # New file: C:_Delphi_Compiled\HomeSuite\Debug\indbo\computerfladskaerm\968ED02C-21B5-4582-8A49-8463E01ADCB3.pdf
30-05-2013 08:10:34:841 # Old file: C:_Delphi_Compiled\HomeSuite\Debug\Indbo\computerfladskaerm\968ED02C-21B5-4582-8A49-8463E01ADCB3.pdf
and as far as I can tell these names are the same
function DocumentHandle(aFile, aFolder, aGuid: string; aAction: TDocumentAction): string;
const
CopyMsg = 'Der findes allerede en fil med det navn!' + sLineBreak +
'Filen omdøbes derfor til et unikt navn';
var
NewFile: string;
begin
Result := aFile;
try
NewFile := ExtractFileName(aFile);
NewFile := aFolder + NewFile;
if Trim(NewFile) = Trim(aFile) then
Exit;
if FileExists(NewFile) then
begin
NewFile := ExtractFileExt(aFile);
NewFile := aFolder + CleanGuid(aGuid) + NewFile;
MessageDlg(CopyMsg, mtWarning, [mbOk], 0);
end;
case aAction of
daCopy:
begin
if CopyFile(PwideChar(aFile), PwideChar(NewFile), False) then
Result := NewFile;
end;
daMove:
begin
if MoveFile(PwideChar(aFile), PwideChar(NewFile)) then
Result := NewFile;
end;
end;
except
on E: exception do
Logfile.Error('U_Documents.DocumentHandle: ' + E.Message);
end;
end;

Comparison is CaseSensitive you have indbo vs. Indbo in your filenames.
You could compare e.g.
UpperCase(f1)=UpperCase(f2)
or
if SameText(f1,f2) then ...

Rather than comparing strings, which can lead to false positives, you could alternatively convert the file paths to PIDLs using SHParseDisplayName() or IShellFolder.ParseDisplayName(), and then compare those using IShellFolder.CompareIDs(). That would allow you to not only compare files of mixed cases, but also compare short vs long file names, etc.

It looks like you're keeping garbage data in your wide string after the meaningful part, can you try Length(aMessage) on both the string and find out if length is same..

Related

Why could CreateFile fail on a network drive

I have one installation that behaves VERY oddly... Every time we try to copy something on a network drive we check accessibility with code like this:
procedure TForm1.TestAccess;
var fn : string;
hdl : THandle;
res : boolean;
dir : string;
flags : Cardinal;
begin
dir := edDir.Text;
flags := FILE_FLAG_DELETE_ON_CLOSE or FILE_FLAG_NO_BUFFERING or FILE_ATTRIBUTE_HIDDEN;
fn := FindUnusedFileName( IncludeTrailingPathDelimiter( dir ) + IntToStr( Random(10000) ) + '.tst' );
memLog.Lines.Add('Try to create file: ' + fn);
hdl := CreateFile( PChar(fN), GENERIC_READ or GENERIC_WRITE, FILE_SHARE_DELETE, nil, CREATE_NEW,
flags, 0 );
res := hdl <> INVALID_HANDLE_VALUE;
if not res then
begin
memLog.Lines.Add('Error: ' + SysErrorMessage(GetLastError));
end
else
memLog.Lines.Add('Success');
if res then
CloseHandle(hdl);
end;
where memLog is a TMemo and edDir is simply an edit field.
Now here is the strange part... I get an access denied on that system meaning uploading will fail most of the time (most of the time is the strange part here).
Another thing is that in a first attempt I used the JvDirectoryEdit control. In that case the result is twofold... If I enter the directory (UNC Path) there without a backslash I get the access denied too. If I enter a final backslash and the combo box window pops up showing the content (aka directories) in that folder it finally works!!!
So... First: has anyone a clue what the problem might be and do I something wrong here?
I have used this procedure in the past to check if a directory is read-only, maybe it can be useful:
FUNCTION DirRO(NomeCartella : String) : Boolean;
VAR VarFile : TextFile;
NomCart : String;
BEGIN
Result := False;
If NomeCartella[Length(NomeCartella)]='\'
Then NomCart := NomeCartella
Else NomCart := NomeCartella+'\';
Try AssignFile(VarFile,NomCart+'^ghi.kol');
{$I-}
Rewrite(VarFile);
{$I-}
If IOResult<>0 Then
Begin
Result := True;
Exit;
End;
CloseFile(VarFile);
Erase(VarFile);
Except Result := True;
End;
END;

Access denied in RenameFile after FileExists

I´m regularly importing files from a path into an application with Delphi 10.2. After each successful import I want to move the current file to a new path(SuccessPath). It´s possible to have file names that already exist in the SuccessPath. That´s why I first check if the file name exists. If thats the case I append an index to the filename (e.g. test.txt changes to test_2.txt).
In some cases RenameFile returns false and GetLastError returns Access denied. It´s probably an unclosed file handle. Do I need to close a handle after calling FileExists? How would I do so?
Here´s my example code:
procedure TDemo.Execute;
begin
MoveFileWithRenameDuplicates('C:\temp\test.txt', 'C:\temp\new\');
end;
procedure TDemo.MoveFileWithRenameDuplicates(oldFile, newPath: string);
var
lNewFile: string;
i: integer;
begin
lNewFile := newPath + TPath.GetFileName(oldFile);
i := 0;
repeat
if FileExists(lNewFile) then
begin
i := i + 1;
lNewFile := newPath + TPath.GetFileNameWithoutExtension(oldFile) + '_' + IntToStr(i) + TPath.GetExtension(oldFile);
end;
until not FileExists(lNewFile);
WriteLn('lNewFile=' + lNewFile);
if not RenameFile(oldFile, lNewFile) then
WriteLn(SysErrorMessage(GetLastError));
end;
I changed my loop like Remy suggested. I also realized that the appended index didn´t actually increment so I changed that aswell:
procedure TDemo.MoveFileWithRenameDuplicates(oldFile, newPath: string);
var
lNewFile: string;
i: integer;
begin
lNewFile := newPath + TPath.GetFileName(oldFile);
i := 0;
while not RenameFile(oldFile, lNewFile) do
begin
if FileExists(lNewFile) then
begin
lNewFile := newPath
+ TRegEx.Match(TPath.GetFileNameWithoutExtension(oldFile), '/^(?''name''[^_]*)_\d*$', [roIgnoreCase]).Groups['name'].Value
+ '_' + IntToStr(i) + TPath.GetExtension(oldFile);
end
else
begin
WriteLn(SysErrorMessage(GetLastError));
break;
end;
end;
end;
This seems to have fixed my problem.

Delphi go up a directory using ExtractFilePath

I'm using this code:
str := ExtractFilePath(ParamStr(0)) + '\Connection.ini';
to get the path of an .ini file, but I want to go up by 2 directories, so the .ini file doesn't sit in the DEBUG folder.
I tried this:
str := ExtractFilePath(ParamStr(0)) + '\..\..\Connection.ini';
But didn't work...
There are quite a few possibilities to go one directory up. Some of them include:
str := ExtractFilePath(ExtractFilePath(ParamStr(0))) + '\Connection.ini';
or
str := IncludeTrailingPathDelimiter(ExtractFilePath(ExtractFilePath(ParamStr(0)))) + 'Connection.ini';
or
str := ExtractFilePath(ParamStr(0)) + '\..\Connection.ini';
I use this function:
TYPE DirStr = STRING;
TYPE CpuWord = Cardinal;
FUNCTION EXECPATH : DirStr;
BEGIN
Result:=IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))
END;
FUNCTION XPATH : DirStr;
VAR
P : CpuWord;
BEGIN
Result:=EXECPATH;
{$IFDEF WINDOWS }
{$IFDEF CPU64BITS }
P:=POS('\WIN64\',UpperCase(Result));
{$ELSE }
P:=POS('\WIN32\',UpperCase(Result));
{$ENDIF }
IF P>0 THEN SetLength(Result,P)
{$ENDIF }
END;
That way, it'll work both when the file is within the default directory when compiling from the IDE and when run from an installation directory.
Use EXECPATH if you want the true directory that the .EXE file resides in, and XPATH is you want the "logical" directory (ie. if within \Win32\Debug and the like, step out of this).
Adapting the answer from Ondrej, I used this method:
function GetParentFolder(folder: String; const cLevels: BYTE = 1) : String;
var
parent: String;
level: BYTE;
begin
// Given "C:\Parent\Child\" or "C:\Parent\Child\MyFile.txt", return "C:\Parent\"
if (cLevels > 0) then
begin
parent := ExcludeTrailingPathDelimiter(folder);
level := 0;
while (level < cLevels) do
begin
parent := ExcludeTrailingPathDelimiter(ExtractFileDir(ExtractFilePath(parent)));
Inc(level);
end;
Result := IncludeTrailingPathDelimiter(parent);
end
else
Result := folder;
end;
And use it like this:
str := GetParentFolder(ParamStr(0), 2) + 'Connection.ini';
The advantage, IMO, is that this works with both a full path or just a directory (without filename). Moreover, you can navigate any number of levels up as required.

How to extract the first instance of unique strings

I need to extract a list of unique items from 12 years' worth of consistent computer-generated one-per day text files. The filenames vary only by the included date, so it is easy to generate the required name in code. They consist of a list of all the aircraft movements at my local airport during the given day, in time order. Naturally, the same aircraft come and go many times, and the objective is to loop through the files, pick out the first instance of when each individual aircraft appears (the first visit or FV) copy it to a list and then ignore it from then on. The result should be a list of all the first visits in date order. Should be simple, but... My program is small so I am including the entire implementation code.
procedure TForm1.FormCreate(Sender: TObject);
begin
FileDate := StrToDate('01/01/2007');
FName := 'E:LGW Reports/SBSLGW2007-01-01.txt'; //1st file to be read
FDStr := copy(FName, 21, 10);
TempList := TStringList.Create; //temp holder for file contents
FVCheckList := TStringList.Create; //holds unique identifier (UID)
FVCheckList.Sorted := TRUE;
FVCheckList.Duplicates := dupIgnore;
FVList:= TStringList.Create; //the main output
end;
procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
begin
Memo1.Lines.Append('Started');
Repeat
TempList.Clear;
TempList.LoadFromFile(FName);
for i := 1 to TempList.Count-1 do
begin
Line := TempList.Strings[i];
//create a //create a Unique identifier (UID) from elements in Line
Serial := Trim(Copy(Line, 22, 9));
MsnPos1 := Pos('[', Line) + 1;
MsnPos2 := Pos(']', Line);
Msn := copy(Line, MsnPos1, (MsnPos2 - MsnPos1));
UID := Serial + '/' + Msn;
//
if (FVCheckList.IndexOf(UID) < 0) then
begin
FVCheckList.Append(UID);
//Add date of file to Line, otherwise it gives no clue when FV was
FVList.Append(FormatDateTime('YYYY-MM-DD', FileDate) + ' ' + Line);
FileDate := IncDay(FileDate, 1);
FName := 'E:LGW Reports/SBSLGW' + FormatDateTime('YYYY-MM-DD', FileDate) + '.txt';
end;
end;
Until FileExists(FName) = FALSE;
FVCheckList.SaveToFile('E:LGW Reports/First Visit Checklist.txt');
FVList.SaveToFile('E:LGW Reports/First Visits.txt');
Memo1.Lines.Append('Finished');
Memo1.Lines.SaveToFile('E:LGW Reports/Files parsed.txt');
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
TempList.Free;
FVCheckList.Free;
FVList.Free;
end;
There are no compiler errors, it runs to completion in seconds and produces the two text files specified, correctly formatted. The big problem is that the lines actually listed in FVList are not always the very first visit of the aircraft, they can be the first, the most recent or somewhere in between. I cannot see any obvious clue as to why the wrong instance is appearing: if my code is right, then something is wrong with the functioning of TStringList FVCheckList. The fault is far more likely to be something I have overlooked, or my understanding of how .dupIgnore works, or maybe my looping isn't working as it should.
I should be very grateful for any practical help. Many thanks in advance.
Repeat
...
Until FileExists(FName) = FALSE;
Should be
While FileExists(FName) = TRUE do
Begin
End;
If the first 2007-01-01 file does not exist, your code will crash on the first LoadFromFile() since you don't check for the file's existence before loading it, unlike with the subsequent files.
Otherwise, I would suggest sticking with repeat but assign FName at the top of each loop iteration instead of initializing it outside the loop and then reassigning at the bottom of each iteration. No need to duplicate efforts.
If you check IndexOf() manually, you don't need to use Sorted or dupIgnore at all. This is what you should be doing in this situation. When dupIgnore ignores a new string, Append() doesn't tell you that the string was ignored. To do that, you would have to check whether the Count was actually increased or not.
Inside the outer loop, the reassignment of FileDate and FName should be outside of the inner for loop,not inside the for loop at all.
Try this instead:
procedure TForm1.FormCreate(Sender: TObject);
begin
FileDate := EncodeDate(2007,1,1);
FDStr := FormatDateTime('YYYY-MM-DD', FileDate);
TempList := TStringList.Create; //temp holder for file contents
FVCheckList := TStringList.Create; //holds unique identifier (UID)
FVList := TStringList.Create; //the main output
end;
procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
begin
Memo1.Lines.Append('Started');
Repeat
FName := 'E:LGW Reports/SBSLGW' + FormatDateTime('YYYY-MM-DD', FileDate) + '.txt';
if not FileExists(FName) then Break;
Memo1.Lines.Append(FName)
TempList.LoadFromFile(FName);
for i := 1 to TempList.Count-1 do
begin
Line := TempList.Strings[i];
//create a Unique identifier (UID) from elements in Line
Serial := Trim(Copy(Line, 22, 9));
MsnPos1 := Pos('[', Line) + 1;
MsnPos2 := PosEx(']', Line, MsnPos1);
Msn := copy(Line, MsnPos1, (MsnPos2 - MsnPos1));
UID := Serial + '/' + Msn;
if FVCheckList.IndexOf(UID) = -1 then
begin
FVCheckList.Append(UID);
//Add date of file to Line, otherwise it gives no clue when FV was
FVList.Append(FormatDateTime('YYYY-MM-DD', FileDate) + ' ' + Line);
end;
end;
FileDate := IncDay(FileDate, 1);
end;
FVCheckList.SaveToFile('E:LGW Reports/First Visit Checklist.txt');
FVList.SaveToFile('E:LGW Reports/First Visits.txt');
Memo1.Lines.Append('Finished');
Memo1.Lines.SaveToFile('E:LGW Reports/Files parsed.txt');
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
TempList.Free;
FVCheckList.Free;
FVList.Free;
end;

How to delete a specific line from a text file in Delphi

I have a text file with user information stored in it line by line. Each line is in the format: UserID#UserEmail#UserPassword with '#' being the delimiter.
I have tried to use this coding to perform the task:
var sl:TStringList;
begin
sl:=TStringList.Create;
sl.LoadFromFile('filename');
sl.Delete(Index);
sl.SaveToFile('filename');
sl.free;
end;
But I'm not sure what to put in the "index" space.
Is there any way I can receive the User ID as input and then delete the line of text from the text file that has this user ID in? Any help would be appreciated.
You can set the NameValueSeparator to # then use IndexOfName to find the user, as long as the username is the first value in the file.
sl.NameValueSeparator := '#';
Index := sl.IndexOfName('455115')
So in your example, like so
var sl:TStringList;
begin
sl:=TStringList.Create;
sl.LoadFromFile('filename');
sl.NameValueSeparator := '#';
Index := sl.IndexOfName('455115')
if (Index <> -1) then
begin
sl.Delete(Index);
sl.SaveToFile('filename');
end;
sl.free;
end;
This may be slow on large files as IndexOfName loops though each line in the TStringList and checks each string in turn until it finds a match.
Disclaimer: Tested/ works with Delphi 2007, Delphi 7 may be diffrent.
I don't see why so many people make this so hard. It is quite simple:
function ShouldDeleteLine(const UserID, Line: string): Boolean;
begin
// Remember: Pos(Needle, Haystack)
Result := Pos(UserID + '#', Line) = 1; // always 1-based!
end;
procedure DeleteLinesWithUserID(const FileName, UserID: string);
var
SL: TStringList;
I: Integer;
begin
if not FileExists(FileName) then
Exit;
SL := TStringList.Create;
try
SL.LoadFromFile(FileName); // Add exception handling for the
// case the file does not load properly.
// Always work backward when deleting items, otherwise your index
// may be off if you really delete.
for I := SL.Count - 1 downto 0 do
if ShouldDeleteLine(SL[I], UserID) then
begin
SL.Delete(I);
// if UserID is unique, you can uncomment the following line.
// Break;
end;
SL.SaveToFile(FileName);
finally
SL.Free;
end;
end;
As Arioch'The says, if you save to the same file name, you risk losing your data when the save fails, so you can do something like
SL.SaveToFile(FileName + '.dup');
if FileExists(FileName + '.old') then
DeleteFile(FileName + '.old');
RenameFile(FileName, FileName + '.old');
RenameFile(FileName + '.dup', FileName);
That keeps a backup of the original file as FileName + '.old'.
Explanations
Working backward
Why work backward? Because if you have the following items
A B C D E F G
^
And you delete the item at ^, then the following items will shift downward:
A B C E F G
^
If you iterate forward, you will now point to
A B C E F G
^
and E is never examined. If you go backward, then you will point to:
A B C E F G
^
Note that E, F and G were examined already, so now you will indeed examine the next item, C, and you won't miss any. Also, if you go upward using 0 to Count - 1, and delete, Count will become one less and at the end, you will try to access past the boundary of the list. This can't happen if you work backwards using Count - 1 downto 0.
Using + '#'
If you append '#' and test for Pos() = 1, you will be sure to catch the entire UserID up to the delimiter, and not a line with a user ID that only contains the UserID you are looking for. IOW, if UserID is 'velthuis', you don't want to delete lines like 'rudyvelthuis#rvelthuis01#password' or 'velthuisresidence#vr#password2', but you do want to delete 'velthuis#bla#pw3'.
E.g. when looking for a user name, you look for '#' + UserName + '#' for the same reason.
There is the only way to actually "delete a line from the text file" - that is to create a new file with changed content, to REWRITE it.
So you better just do it explicitly.
And don't you forget about protecting from errors. Your current code might just destroy the file and leak memory, if any error occurs...
var sl: TStringList;
s, prefix: string;
i: integer; okay: Boolean;
fs: TStream;
begin
prefix := 'UserName' + '#';
okay := false;
fs := nil;
sl:=TStringList.Create;
Try /// !!!!
sl.LoadFromFile('filename');
fs := TFileStream.Create( 'filename~new', fmCreate or fmShareExclusive );
for i := 0 to Prev(sl.Count) do begin
s := sl[ i ];
if AnsiStartsStr( prefix, Trim(s) ) then
continue; // skip the line - it was our haunted user
s := s + ^M^J; // add end-of-line marker for saving to file
fs.WriteBuffer( s[1], length(s)*SizeOf(s[1]) );
end;
finally
fs.Free;
sl.Free;
end;
// here - and only here - we are sure we successfully rewritten
// the fixed file and only no are able to safely delete old file
if RenameFile( 'filename' , 'filename~old') then
if RenameFile( 'filename~new' , 'filename') then begin
okay := true;
DeleteFile( 'filename~old' );
end;
if not okay then ShowMessage(' ERROR!!! ');
end;
Note 1: See if check for username should be case-sensitive or case-ignoring:
http://www.freepascal.org/docs-html/rtl/strutils/ansistartsstr.html
http://www.freepascal.org/docs-html/rtl/strutils/ansistartstext.html
Note 2: in Delphi 7 SizeOf( s[1] ) is always equal to one because string is an alias to AnsiString. But in newer Delphi version it is not. It might seems tedious and redundant - but it might save a LOT of headache in future. Even better would be to have a temporary AnsiString type variable like a := AnsiString( s + ^m^J ); fs.WriteBuffer(a[1],Length(a));
So far everyone has been suggesting the use for a For..Then Loop but can I suggest a Repeat..While.
The traditional For..Loop is a good option but could be inefficient if you have a long list of Usernames (they are usually unique). Once found and deleted the For Loop continues until the end of the list. That's ok if you have a small list but if you have 500,000 Usernames and the one you want is at position 10,000 there is no reason to continue beyond that point.
Therefore, try this.
Function DeleteUser(Const TheFile: String; Const TheUserName: String): Boolean;
Var
CurrentLine: Integer;
MyLines: TStringlist;
Found: Boolean;
Eof: Integer;
Begin
MyLines := TStringlist.Create;
MyLines.LoadFromFile(TheFile);
CurrentLine := 0;
Eof := Mylines.count - 1;
Found := false;
Repeat
If Pos(UpperCase(TheUserName), UpperCase(MyLines.Strings[CurrentLine])) = 1 Then
Begin
MyLines.Delete(CurrentLine);
Found := True;
End;
Inc(CurrentLine);
Until (Found) Or (CurrentLine = Eof); // Jump out when found or End of File
MyLines.SaveToFile(TheFile);
MyLines.Free;
result := Found;
End;
Once called the function returns True or False indicating the Username was deleted or not.
If Not DeleteUsername(TheFile,TheUsername) then
ShowMessage('User was not found, what were you thinking!');
Just for fun, here's a compact solution, which I like for its readability.
const fn = 'myfile.txt';
procedure DeleteUser(id: integer);
var s:string; a:TStringDynArray;
begin
for s in TFile.ReadAllLines(fn) do
if not s.StartsWith(id.ToString + '#') then
a := a + [s];
TFile.WriteAllLines(fn, a);
end;
Obviously it's not the most efficient solution. This could run faster by not appending single items to the array, or by caching the search string.
And to search for other fields, you could use s.split(['#'])[0] to find the username, s.split(['#'])[1] for email, etc.
For those who like one-liners. This works too:
const fn = 'users.txt';
procedure DeleteUserRegExp(id: string);
begin
TFile.WriteAllText(fn,TRegEx.Replace(TFile.ReadAllText(fn),''+id+'\#.*\r\n',''))
end;
Explanation
It loads the content of a file into a string.
The string is sent to TRegEx.Replace
The regular expression searches for the username followed by the hash sign, then any character, and then a CRLF. It replaces it with an empty string.
The resulting string is then written to the original file
This is just for fun though, because I saw long code where I thought that this would be possible with a single line of code.

Resources