FIX: Try to fix bug [2923641] "Change case of filenames on VFAT-volume fails".

This commit is contained in:
cobines 2010-01-19 20:50:08 +00:00
commit 37e6610a7e
7 changed files with 352 additions and 70 deletions

View file

@ -734,8 +734,7 @@ begin
for c:=0 to lsvwFile.Items.Count-1 do
with lsvwFile.Items do
begin
if RenameFile(FFileSource, TFile(Item[c].Data),
Item[c].SubItems[1]+pathdelim+Item[c].SubItems[0], True) = True then
if RenameFile(FFileSource, TFile(Item[c].Data), Item[c].SubItems[0], True) = True then
begin
Item[c].Caption := Item[c].SubItems[0]; // write the new name to table
TFile(Item[c].Data).Name := Item[c].SubItems[0]; // and to the file object
@ -1020,4 +1019,4 @@ end;
initialization
{$I fmultirename.lrs}
end.

View file

@ -23,6 +23,8 @@ type
TFilePropertiesDescriptions = array of String;//TFileProperty;
EInvalidFileProperty = class(Exception);
// Forward declarations.
IFilePropertyFormatter = interface;
@ -67,6 +69,8 @@ type
private
FName: UTF8String; // only name, no path
procedure SetName(NewName: UTF8String);
public
constructor Create; override;
constructor Create(Name: UTF8String); virtual; overload;
@ -79,7 +83,7 @@ type
function Format(Formatter: IFilePropertyFormatter): String; override;
property Value: UTF8String read FName write FName;
property Value: UTF8String read FName write SetName;
end;
TFileSizeProperty = class(TFileProperty)
@ -341,6 +345,17 @@ begin
Result := Formatter.FormatFileName(Self);
end;
procedure TFileNameProperty.SetName(NewName: UTF8String);
var
i: Integer;
begin
for i := 1 to Length(NewName) do
if NewName[i] in AllowDirectorySeparators then
raise EInvalidFileProperty.Create('Name cannot have directory separators');
FName := NewName;
end;
// ----------------------------------------------------------------------------
constructor TFileSizeProperty.Create;

View file

@ -508,7 +508,6 @@ uses
uFileSourceOperationOptions,
uFileSourceCalcStatisticsOperation,
uFileSystemFile,
uFileSystemFileSource,
fColumnsSetConf,
uKeyboard,
uFileSourceUtil
@ -1838,7 +1837,6 @@ procedure TColumnsFileView.edtRenameKeyDown(Sender: TObject; var Key: Word;
var
NewFileName: String;
OldFileNameAbsolute: String;
NewFileNameAbsolute: String;
begin
case Key of
VK_ESCAPE:
@ -1855,25 +1853,21 @@ begin
NewFileName := edtRename.Text;
OldFileNameAbsolute := edtRename.Hint;
NewFileNameAbsolute := ExtractFilePath(OldFileNameAbsolute) + NewFileName;
if (FileSource.IsClass(TFileSystemFileSource)) and mbFileExists(NewFileNameAbsolute) then
begin
if MsgBox(Format(rsMsgFileExistsRwrt, [NewFileName]),
[msmbYes, msmbNo], msmbYes, msmbNo) = mmrNo then
try
if RenameFile(FileSource, ActiveFile, NewFileName, True) = True then
begin
Exit;
end;
edtRename.Visible:=False;
LastActive := NewFileName;
SetFocus;
end
else
msgError(Format(rsMsgErrRename, [ExtractFileName(OldFileNameAbsolute), NewFileName]));
except
on e: EInvalidFileProperty do
msgError(Format(rsMsgErrRename + ':' + LineEnding + '%s (%s)', [ExtractFileName(OldFileNameAbsolute), NewFileName, rsMsgInvalidFileName, e.Message]));
end;
if RenameFile(FileSource, ActiveFile, NewFileNameAbsolute, True) = True then
begin
edtRename.Visible:=False;
LastActive := NewFileName;
SetFocus;
end
else
msgError(Format(rsMsgErrRename, [ExtractFileName(OldFileNameAbsolute), NewFileName]));
end;
{$IFDEF LCLGTK2}

View file

@ -15,6 +15,8 @@ uses
type
TSetFilePropertyResult = (sfprSuccess, sfprError, sfprSkipped);
TFileSourceSetFilePropertyOperationStatistics = record
CurrentFile: String;
TotalFiles: Int64;
@ -80,7 +82,7 @@ type
procedure UpdateStatisticsAtStartTime; override;
procedure SetProperties(aFile: TFile; aTemplateFile: TFile);
function SetNewProperty(aFile: TFile; aTemplateProperty: TFileProperty): Boolean; virtual abstract;
function SetNewProperty(aFile: TFile; aTemplateProperty: TFileProperty): TSetFilePropertyResult; virtual abstract;
function GetErrorString(aFile: TFile; aProperty: TFileProperty): String;
@ -245,7 +247,7 @@ var
templateProperty: TFileProperty;
bRetry: Boolean;
sMessage, sQuestion: String;
Success: Boolean;
SetResult: TSetFilePropertyResult;
{$IFNDEF AssignFileNameProperty}
TargetName: UTF8String; // for reporting errors when setting fpName property
{$ENDIF}
@ -256,7 +258,7 @@ begin
begin
repeat
bRetry := False;
Success := True;
SetResult := sfprSuccess;
{$IFNDEF AssignFileNameProperty}
if prop = fpName then
@ -265,13 +267,13 @@ begin
begin
templateProperty := TFileNameProperty.Create(aTemplateFile.Name);
TargetName := aTemplateFile.Name;
Success := SetNewProperty(aFile, templateProperty);
SetResult := SetNewProperty(aFile, templateProperty);
FreeAndNil(templateProperty);
end
else if Assigned(NewProperties[fpName]) then
begin
TargetName := (NewProperties[fpName] as TFileNameProperty).Value;
Success := SetNewProperty(aFile, NewProperties[fpName]);
SetResult := SetNewProperty(aFile, NewProperties[fpName]);
end;
end
else
@ -287,10 +289,10 @@ begin
// Check if there is a new property to be set.
if Assigned(templateProperty) then
Success := SetNewProperty(aFile, templateProperty);
SetResult := SetNewProperty(aFile, templateProperty);
end;
if not Success then
if SetResult = sfprError then
begin
{$IFNDEF AssignFileNameProperty}
if prop = fpName then

View file

@ -24,8 +24,10 @@ type
// Options.
FSymLinkOption: TFileSourceOperationOptionSymLink;
function RenameFile(const OldName: UTF8String; NewName: UTF8String): TSetFilePropertyResult;
protected
function SetNewProperty(aFile: TFile; aTemplateProperty: TFileProperty): Boolean; override;
function SetNewProperty(aFile: TFile; aTemplateProperty: TFileProperty): TSetFilePropertyResult; override;
public
constructor Create(aTargetFileSource: IFileSource;
@ -43,8 +45,14 @@ type
implementation
uses
uGlobs, uOSUtils, uLng, uDateTimeUtils, uFileSystemUtil,
uFileSourceOperationUI;
uGlobs, uOSUtils, uDCUtils, uLng, uDateTimeUtils, uFileSystemUtil, uTypes,
uFileSourceOperationUI
{$IF DEFINED(MSWINDOWS)}
, LCLProc
{$ELSEIF DEFINED(UNIX)}
, BaseUnix
{$ENDIF}
;
constructor TFileSystemSetFilePropertyOperation.Create(aTargetFileSource: IFileSource;
var theTargetFiles: TFiles;
@ -130,61 +138,83 @@ begin
end;
function TFileSystemSetFilePropertyOperation.SetNewProperty(aFile: TFile;
aTemplateProperty: TFileProperty): Boolean;
aTemplateProperty: TFileProperty): TSetFilePropertyResult;
begin
Result := True;
Result := sfprSuccess;
try
case aTemplateProperty.GetID of
fpName:
if (aTemplateProperty as TFileNameProperty).Value <> aFile.Name then
begin
Result := mbRenameFile(
Result := RenameFile(
aFile.FullPath,
(aTemplateProperty as TFileNameProperty).Value);
end;
end
else
Result := sfprSkipped;
fpAttributes:
if (aTemplateProperty as TFileAttributesProperty).Value <>
(aFile.Properties[fpAttributes] as TFileAttributesProperty).Value then
begin
Result := mbFileSetAttr(
if mbFileSetAttr(
aFile.FullPath,
(aTemplateProperty as TFileAttributesProperty).Value) = 0;
end;
(aTemplateProperty as TFileAttributesProperty).Value) <> 0 then
begin
Result := sfprError;
end;
end
else
Result := sfprSkipped;
fpModificationTime:
if (aTemplateProperty as TFileModificationDateTimeProperty).Value <>
(aFile.Properties[fpModificationTime] as TFileModificationDateTimeProperty).Value then
begin
Result := mbFileSetTime(
if not mbFileSetTime(
aFile.FullPath,
DateTimeToFileTime((aTemplateProperty as TFileModificationDateTimeProperty).Value),
0,
0);
end;
0) then
begin
Result := sfprError;
end;
end
else
Result := sfprSkipped;
fpCreationTime:
if (aTemplateProperty as TFileCreationDateTimeProperty).Value <>
(aFile.Properties[fpCreationTime] as TFileCreationDateTimeProperty).Value then
begin
Result := mbFileSetTime(
if not mbFileSetTime(
aFile.FullPath,
0,
DateTimeToFileTime((aTemplateProperty as TFileCreationDateTimeProperty).Value),
0);
end;
0) then
begin
Result := sfprError;
end;
end
else
Result := sfprSkipped;
fpLastAccessTime:
if (aTemplateProperty as TFileLastAccessDateTimeProperty).Value <>
(aFile.Properties[fpLastAccessTime] as TFileLastAccessDateTimeProperty).Value then
begin
Result := mbFileSetTime(
if not mbFileSetTime(
aFile.FullPath,
0,
0,
DateTimeToFileTime((aTemplateProperty as TFileLastAccessDateTimeProperty).Value));
end;
DateTimeToFileTime((aTemplateProperty as TFileLastAccessDateTimeProperty).Value)) then
begin
Result := sfprError;
end;
end
else
Result := sfprSkipped;
else
raise Exception.Create('Trying to set unsupported property');
@ -194,14 +224,161 @@ begin
on e: EConvertError do
begin
if not gSkipFileOpError then
if AskQuestion(rsMsgLogError + e.Message, '', [fsourSkip, fsourAbort],
fsourSkip, fsourAbort) = fsourAbort then
begin
RaiseAbortOperation;
case AskQuestion(rsMsgLogError + e.Message, '', [fsourSkip, fsourAbort],
fsourSkip, fsourAbort) of
fsourSkip:
Result := sfprSkipped;
fsourAbort:
RaiseAbortOperation;
end;
end;
end;
end;
function TFileSystemSetFilePropertyOperation.RenameFile(const OldName: UTF8String; NewName: UTF8String): TSetFilePropertyResult;
function AskIfOverwrite(Attrs: TFileAttrs): TFileSourceOperationUIResponse;
var
sQuestion: String;
begin
if uOSUtils.FPS_ISDIR(Attrs) then
sQuestion := rsMsgFolderExistsRwrt
else
sQuestion := rsMsgFileExistsRwrt;
Result := AskQuestion(Format(sQuestion, [NewName]), '',
[fsourYes, fsourNo, fsourAbort], fsourYes, fsourNo);
end;
var
{$IFDEF UNIX}
tmpFileName: UTF8String;
OldFileStat, NewFileStat: stat;
{$ELSE}
NewFileAttrs: TFileAttrs;
{$ENDIF}
begin
if FileSource.GetPathType(NewName) <> ptAbsolute then
NewName := ExtractFilePath(OldName) + NewName;
if OldName = NewName then
Exit(sfprSkipped);
{$IFDEF UNIX}
if fpLstat(OldName, OldFileStat) <> 0 then
Exit(sfprError);
// Check if target file exists.
if fpLstat(NewName, NewFileStat) = 0 then
begin
// Check if source and target are the same files (same inode and same device).
if (OldFileStat.st_ino = NewFileStat.st_ino) and
(OldFileStat.st_dev = NewFileStat.st_dev) then
begin
// Check number of links.
// If it is 1 then source and target names most probably differ only
// by case on a case-insensitive filesystem. Direct rename() in such case
// fails on Linux, so we use a temporary file name and rename in two stages.
// If number of links is more than 1 then it's enough to simply unlink
// the source file, since both files are technically identical.
// (On Linux rename() returns success but doesn't do anything
// if renaming a file to its hard link.)
// We cannot use st_nlink for directories because it means "number of
// subdirectories"; hard links to directories are not supported on Linux
// or Windows anyway (on MacOSX they are). Therefore we always treat
// directories as if they were a single link and rename them using temporary name.
if (NewFileStat.st_nlink = 1) or BaseUnix.fpS_ISDIR(NewFileStat.st_mode) then
begin
tmpFileName := GetTempName(OldName);
if FpRename(OldName, tmpFileName) = 0 then
begin
if fpLstat(NewName, NewFileStat) = 0 then
begin
// We have renamed the old file but the new file name still exists,
// so this wasn't a single file on a case-insensitive filesystem
// accessible by two names that differ by case.
FpRename(tmpFileName, OldName); // Restore old file.
{$IFDEF DARWIN}
// If it was a directory with multiple hard links then fall through
// to asking for overwrite and unlinking source link.
if not (BaseUnix.fpS_ISDIR(NewFileStat.st_mode) and (NewFileStat.st_nlink > 1)) then
{$ENDIF}
Exit(sfprError);
end
else if FpRename(tmpFileName, NewName) = 0 then
begin
Exit(sfprSuccess);
end
else
begin
FpRename(tmpFileName, OldName); // Restore old file.
Exit(sfprError);
end;
end
else
Exit(sfprError);
end;
// Both names are hard links to the same file.
case AskIfOverwrite(NewFileStat.st_mode) of
fsourYes: ; // continue
fsourNo:
Exit(sfprSkipped);
fsourAbort:
RaiseAbortOperation;
end;
// Multiple links - simply unlink the source file.
if fpUnLink(OldName) = 0 then
Result := sfprSuccess
else
Result := sfprError;
Exit;
end
else
begin
case AskIfOverwrite(NewFileStat.st_mode) of
fsourYes: ; // continue
fsourNo:
Exit(sfprSkipped);
fsourAbort:
RaiseAbortOperation;
end;
end;
end;
if FpRename(OldName, NewName) = 0 then
{$ELSE}
// Windows XP doesn't allow two filenames that differ only by case (even on NTFS).
if UTF8LowerCase(OldName) <> UTF8LowerCase(NewName) then
begin
NewFileAttrs := mbFileGetAttr(NewName);
if NewFileAttrs <> faInvalidAttributes then // If target file exists.
begin
case AskIfOverwrite(NewFileAttrs) of
fsourYes: ; // continue
fsourNo:
Exit(sfprSkipped);
fsourAbort:
RaiseAbortOperation;
end;
end;
end;
if mbRenameFile(OldName, NewName) then
{$ENDIF}
Result := sfprSuccess
else
Result := sfprError;
end;
end.

View file

@ -26,7 +26,7 @@ type
FSymLinkOption: TFileSourceOperationOptionSymLink;
protected
function SetNewProperty(aFile: TFile; aTemplateProperty: TFileProperty): Boolean; override;
function SetNewProperty(aFile: TFile; aTemplateProperty: TFileProperty): TSetFilePropertyResult; override;
public
constructor Create(aTargetFileSource: IFileSource;
@ -136,20 +136,23 @@ begin
end;
function TWfxPluginSetFilePropertyOperation.SetNewProperty(aFile: TFile;
aTemplateProperty: TFileProperty): Boolean;
aTemplateProperty: TFileProperty): TSetFilePropertyResult;
var
FileName: UTF8String;
NewAttributes: TFileAttrs;
ftTime: TFileTime;
begin
Result := True;
Result := sfprSuccess;
case aTemplateProperty.GetID of
fpName:
if (aTemplateProperty as TFileNameProperty).Value <> aFile.Name then
begin
Result := WfxRenameFile(FWfxPluginFileSource, aFile, (aTemplateProperty as TFileNameProperty).Value);
end;
if not WfxRenameFile(FWfxPluginFileSource, aFile, (aTemplateProperty as TFileNameProperty).Value) then
Result := sfprError;
end
else
Result := sfprSkipped;
fpAttributes:
if (aTemplateProperty as TFileAttributesProperty).Value <>
@ -160,12 +163,20 @@ begin
with FWfxPluginFileSource.WfxModule do
if aTemplateProperty is TNtfsFileAttributesProperty then
Result:= WfxSetAttr(FileName, NewAttributes)
begin
if not WfxSetAttr(FileName, NewAttributes) then
Result := sfprError;
end
else if aTemplateProperty is TUnixFileAttributesProperty then
Result:= WfxExecuteFile(0, FileName, 'chmod' + #32 + DecToOct(NewAttributes)) = FS_EXEC_OK
begin
if WfxExecuteFile(0, FileName, 'chmod' + #32 + DecToOct(NewAttributes)) <> FS_EXEC_OK then
Result := sfprError;
end
else
raise Exception.Create('Unsupported file attributes type');
end;
end
else
Result := sfprSkipped;
fpModificationTime:
if (aTemplateProperty as TFileModificationDateTimeProperty).Value <>
@ -173,8 +184,11 @@ begin
begin
ftTime := DateTimeToWfxFileTime((aTemplateProperty as TFileModificationDateTimeProperty).Value);
with FWfxPluginFileSource.WfxModule do
Result := WfxSetTime(aFile.FullPath, nil, nil, @ftTime);
end;
if not WfxSetTime(aFile.FullPath, nil, nil, @ftTime) then
Result := sfprError;
end
else
Result := sfprSkipped;
fpCreationTime:
if (aTemplateProperty as TFileCreationDateTimeProperty).Value <>
@ -182,8 +196,11 @@ begin
begin
ftTime := DateTimeToWfxFileTime((aTemplateProperty as TFileCreationDateTimeProperty).Value);
with FWfxPluginFileSource.WfxModule do
Result := WfxSetTime(aFile.FullPath, @ftTime, nil, nil);
end;
if not WfxSetTime(aFile.FullPath, @ftTime, nil, nil) then
Result := sfprError;
end
else
Result := sfprSkipped;
fpLastAccessTime:
if (aTemplateProperty as TFileLastAccessDateTimeProperty).Value <>
@ -191,8 +208,11 @@ begin
begin
ftTime := DateTimeToWfxFileTime((aTemplateProperty as TFileLastAccessDateTimeProperty).Value);
with FWfxPluginFileSource.WfxModule do
Result := WfxSetTime(aFile.FullPath, nil, @ftTime, nil);
end;
if not WfxSetTime(aFile.FullPath, nil, @ftTime, nil) then
Result := sfprError;
end
else
Result := sfprSkipped;
else
raise Exception.Create('Trying to set unsupported property');

View file

@ -234,7 +234,7 @@ function mbDeleteToTrash(const FileName: UTF8String): Boolean;
// 14.05.2009 - this funtion checks 'gvfs-trash' BEFORE deleting. Need for various linux disributives.
function mbCheckTrash(sPath: UTF8String): Boolean;
// ----------------
function mbRenameFile(const OldName, NewName : UTF8String): Boolean;
function mbRenameFile(const OldName: UTF8String; NewName: UTF8String): Boolean;
function mbFileSize(const FileName: UTF8String): Int64;
function FileFlush(Handle: THandle): Boolean;
{ Directory handling functions}
@ -1535,7 +1535,7 @@ begin
end;
{$ELSE}
begin
Result:= fpUnLink(FileName) >= 0;
Result:= fpUnLink(FileName) = 0;
end;
{$ENDIF}
@ -1621,7 +1621,7 @@ end;
// --------------------------------------------------------------------------------
function mbRenameFile(const OldName, NewName: UTF8String): Boolean;
function mbRenameFile(const OldName: UTF8String; NewName: UTF8String): Boolean;
{$IFDEF MSWINDOWS}
var
wOldName,
@ -1632,8 +1632,83 @@ begin
Result:= MoveFileExW(PWChar(wOldName), PWChar(wNewName), MOVEFILE_REPLACE_EXISTING);
end;
{$ELSE}
var
tmpFileName: UTF8String;
OldFileStat, NewFileStat: stat;
begin
Result:= BaseUnix.FpRename(OldNAme, NewName) >= 0;
if GetPathType(NewName) <> ptAbsolute then
NewName := ExtractFilePath(OldName) + NewName;
if OldName = NewName then
Exit(True);
if fpLstat(OldName, OldFileStat) <> 0 then
Exit(False);
// Check if target file exists.
if fpLstat(NewName, NewFileStat) = 0 then
begin
// Check if source and target are the same files (same inode and same device).
if (OldFileStat.st_ino = NewFileStat.st_ino) and
(OldFileStat.st_dev = NewFileStat.st_dev) then
begin
// Check number of links.
// If it is 1 then source and target names most probably differ only
// by case on a case-insensitive filesystem. Direct rename() in such case
// fails on Linux, so we use a temporary file name and rename in two stages.
// If number of links is more than 1 then it's enough to simply unlink
// the source file, since both files are technically identical.
// (On Linux rename() returns success but doesn't do anything
// if renaming a file to its hard link.)
// We cannot use st_nlink for directories because it means "number of
// subdirectories"; hard links to directories are not supported on Linux
// or Windows anyway (on MacOSX they are). Therefore we always treat
// directories as if they were a single link and rename them using temporary name.
if (NewFileStat.st_nlink = 1) or BaseUnix.fpS_ISDIR(NewFileStat.st_mode) then
begin
tmpFileName := GetTempName(OldName);
if FpRename(OldName, tmpFileName) = 0 then
begin
if fpLstat(NewName, NewFileStat) = 0 then
begin
// We have renamed the old file but the new file name still exists,
// so this wasn't a single file on a case-insensitive filesystem
// accessible by two names that differ by case.
FpRename(tmpFileName, OldName); // Restore old file.
{$IFDEF DARWIN}
// If it's a directory with multiple hard links then simply unlink the source.
if BaseUnix.fpS_ISDIR(NewFileStat.st_mode) and (NewFileStat.st_nlink > 1) then
Result := (fpUnLink(OldName) = 0)
else
{$ENDIF}
Result := False;
end
else if FpRename(tmpFileName, NewName) = 0 then
begin
Result := True;
end
else
begin
FpRename(tmpFileName, OldName); // Restore old file.
Result := False;
end;
end
else
Result := False;
end
else
begin
// Multiple links - simply unlink the source file.
Result := (fpUnLink(OldName) = 0);
end;
Exit;
end;
end;
Result := FpRename(OldName, NewName) = 0;
end;
{$ENDIF}
@ -2051,4 +2126,4 @@ finalization
{$ENDIF}
end.
end.