This commit is contained in:
EH 2026-06-19 03:21:56 +00:00 committed by GitHub
commit 2cfa24119b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 148 additions and 11 deletions

View file

@ -69,7 +69,7 @@ implementation
uses
LazUTF8, DCBasicTypes, DCDateTimeUtils, DCStrUtils, DCOSUtils, CTypes,
DCClassesUtf8, DCFileAttributes, DCConvertEncoding;
DCClassesUtf8, DCFileAttributes, DCConvertEncoding{$IFDEF UNIX}, BaseUnix{$ENDIF};
const
READ_BUFFER_SIZE = 131072;
@ -228,12 +228,46 @@ var
TotalBytesToWrite: Int64 = 0;
TargetHandle: PLIBSSH2_SFTP_HANDLE = nil;
Flags: cint = LIBSSH2_FXF_CREAT or LIBSSH2_FXF_WRITE;
{$IFDEF UNIX}
LocalStat: BaseUnix.TStat;
UploadAttrs: LIBSSH2_SFTP_ATTRIBUTES;
LinkTarget: String;
{$ENDIF}
begin
if FCopySCP then begin
Result:= inherited StoreFile(FileName, Restore);
Exit;
end;
{$IFDEF UNIX}
// If the local source is a symlink, recreate it on the remote.
// On failure (server refused), fall through to a normal content upload.
if fpLStat(FDirectFileName, LocalStat) = 0 then
begin
if FPS_ISLNK(LocalStat.st_mode) then
begin
LinkTarget:= fpReadLink(FDirectFileName);
if Length(LinkTarget) > 0 then
begin
// Remove any existing destination; sftp_symlink does not overwrite.
repeat
FLastError:= libssh2_sftp_unlink(FSFTPSession, PAnsiChar(FileName));
if FLastError = LIBSSH2_ERROR_EAGAIN then FSock.CanRead(10);
until FLastError <> LIBSSH2_ERROR_EAGAIN;
repeat
FLastError:= libssh2_sftp_symlink(FSFTPSession, PAnsiChar(LinkTarget), PAnsiChar(FileName));
if FLastError = LIBSSH2_ERROR_EAGAIN then FSock.CanRead(10);
until FLastError <> LIBSSH2_ERROR_EAGAIN;
if FLastError = 0 then
begin
Result:= True;
Exit;
end;
end;
end;
end;
{$ENDIF}
SendStream := TFileStreamEx.Create(FDirectFileName, fmOpenRead or fmShareDenyWrite);
TargetName:= PWideChar(ServerToClient(FileName));
@ -304,6 +338,22 @@ begin
FreeMem(FBuffer);
Result:= FileClose(TargetHandle) and Result;
libssh2_session_set_blocking(FSession, 1);
{$IFDEF UNIX}
if Result then
begin
if FpStat(FDirectFileName, LocalStat) = 0 then
begin
FillChar(UploadAttrs, SizeOf(UploadAttrs), 0);
UploadAttrs.permissions:= LocalStat.st_mode;
UploadAttrs.flags:= LIBSSH2_SFTP_ATTR_PERMISSIONS;
libssh2_sftp_setstat(FSFTPSession, PAnsiChar(FileName), @UploadAttrs);
UploadAttrs.uid:= LocalStat.st_uid;
UploadAttrs.gid:= LocalStat.st_gid;
UploadAttrs.flags:= LIBSSH2_SFTP_ATTR_UIDGID;
libssh2_sftp_setstat(FSFTPSession, PAnsiChar(FileName), @UploadAttrs);
end;
end;
{$ENDIF}
end;
end;
@ -315,6 +365,9 @@ var
RetrStream: TFileStreamEx;
TotalBytesToRead: Int64 = 0;
SourceHandle: PLIBSSH2_SFTP_HANDLE;
{$IFDEF UNIX}
DownloadAttrs: LIBSSH2_SFTP_ATTRIBUTES;
{$ENDIF}
begin
if FCopySCP then begin
Result:= inherited RetrieveFile(FileName, FileSize, Restore);
@ -380,6 +433,20 @@ begin
finally
RetrStream.Free;
libssh2_session_set_blocking(FSession, 1);
{$IFDEF UNIX}
if Result then
begin
if libssh2_sftp_stat(FSFTPSession, PAnsiChar(FileName), @DownloadAttrs) = 0 then
begin
if (DownloadAttrs.flags and LIBSSH2_SFTP_ATTR_PERMISSIONS) <> 0 then
begin
FpChmod(FDirectFileName, DownloadAttrs.permissions and $0FFF);
if (DownloadAttrs.flags and LIBSSH2_SFTP_ATTR_UIDGID) <> 0 then
FpChown(FDirectFileName, DownloadAttrs.uid, DownloadAttrs.gid);
end;
end;
end;
{$ENDIF}
end;
end;
@ -404,6 +471,7 @@ var
Return: Integer;
FindRec: PFindRec absolute Handle;
Attributes: LIBSSH2_SFTP_ATTRIBUTES;
LinkAttrs: LIBSSH2_SFTP_ATTRIBUTES;
AFileName: array[0..1023] of AnsiChar;
AFullData: array[0..2047] of AnsiChar;
begin
@ -425,6 +493,9 @@ begin
FindData.ftLastAccessTime:= TWfxFileTime(UnixFileTimeToWinTime(Attributes.atime));
if (Attributes.permissions and S_IFMT) = S_IFLNK then
begin
// Follow the link to detect if the target is a directory, but keep
// the symlink's own mtime and size for sync comparisons.
LinkAttrs:= Attributes;
if libssh2_sftp_stat(FSFTPSession, PAnsiChar(FindRec.Path + AFileName), @Attributes) = 0 then
begin
if (Attributes.permissions and S_IFMT) = S_IFDIR then
@ -432,8 +503,16 @@ begin
FindData.nFileSizeLow:= 0;
FindData.nFileSizeHigh:= 0;
FindData.dwFileAttributes:= FindData.dwFileAttributes or FILE_ATTRIBUTE_REPARSE_POINT;
end
else
begin
// Restore the symlink's own size (= byte length of link target string).
FindData.nFileSizeLow:= Int64Rec(LinkAttrs.filesize).Lo;
FindData.nFileSizeHigh:= Int64Rec(LinkAttrs.filesize).Hi;
end;
end;
FindData.ftLastWriteTime:= TWfxFileTime(UnixFileTimeToWinTime(LinkAttrs.mtime));
FindData.ftLastAccessTime:= TWfxFileTime(UnixFileTimeToWinTime(LinkAttrs.atime));
end;
end;
end;

View file

@ -137,7 +137,7 @@ begin
TreeBuilder := TFileSystemTreeBuilder.Create(@AskQuestion, @CheckOperationState);
try
ElevateAction:= dupError;
TreeBuilder.SymLinkOption:= fsooslFollow;
TreeBuilder.SymLinkOption:= fsooslDontFollow;
TreeBuilder.BuildFromFiles(SourceFiles);
FSourceFilesTree := TreeBuilder.ReleaseTree;
FStatistics.TotalFiles := TreeBuilder.FilesCount;

View file

@ -369,7 +369,10 @@ begin
Result := ProcessDirectory(aSubNode, AbsoluteTargetFileName)
else
Result := ProcessFile(aSubNode, AbsoluteTargetFileName);
end;
end
else
// Link not followed — pass it to ProcessFile to handle (e.g. SFTP symlink creation).
Result := ProcessFile(aNode, AbsoluteTargetFileName);
end;
function TWfxPluginOperationHelper.ProcessFile(aNode: TFileTreeNode;

View file

@ -236,6 +236,7 @@ implementation
uses
fMain, uDebug, fDiffer, fSyncDirsPerformDlg, uGlobs, LCLType, LazUTF8, LazFileUtils,
uOSForms,
uFileSystemFileSource, uFileSourceOperationOptions, DCDateTimeUtils, SyncObjs,
uDCUtils, uFileSourceUtil, uFileSourceOperationTypes, uShowForm, uAdministrator,
uOSUtils, uLng, uMasks, Math, uClipboard, IntegerList, fMaskInputDlg, uSearchTemplate,
@ -289,13 +290,22 @@ type
end;
procedure ShowSyncDirsDlg(FileView1, FileView2: TFileView);
var
Dlg: TfrmSyncDirsDlg;
begin
if not Assigned(FileView1) then
raise Exception.Create('ShowSyncDirsDlg: FileView1=nil');
if not Assigned(FileView2) then
raise Exception.Create('ShowSyncDirsDlg: FileView2=nil');
with TfrmSyncDirsDlg.Create(Application, FileView1, FileView2) do
Show;
Dlg := TfrmSyncDirsDlg.Create(Application, FileView1, FileView2);
{ Center on the same monitor as the main DC window. }
if Assigned(frmMain) then
with GetFrmMainMonitor do
Dlg.SetBounds(
Left + (Width - Dlg.Width) div 2,
Top + (Height - Dlg.Height) div 2,
Dlg.Width, Dlg.Height);
Dlg.Show;
end;
{ TDrawGrid }
@ -465,7 +475,12 @@ begin
end;
if R.FAction = srsUnknown then
begin
R.FAction := R.FState;
// Mirror asymmetric logic from TFileSyncRec.Recalc:
// in asymmetric mode srsNotEq means left wins → srsCopyRight.
if chkAsymmetric.Checked and (R.FState = srsNotEq) then
R.FAction := srsCopyRight
else
R.FAction := R.FState;
end;
except
on E: Exception do
@ -543,6 +558,38 @@ end;
procedure TFileSyncRec.UpdateState(ignoreDate: Boolean);
var
FileTimeDiff: Integer;
function AreEquivalentLinks: Boolean;
var
LeftTarget, RightTarget: String;
LeftResolved, RightResolved: String;
begin
Result := False;
if not (FFileL.IsLink and FFileR.IsLink) then Exit;
LeftTarget := FFileL.LinkProperty.LinkTo;
RightTarget := FFileR.LinkProperty.LinkTo;
// Fast path: identical link text means semantically identical link.
if LeftTarget = RightTarget then Exit(True);
if (LeftTarget = EmptyStr) or (RightTarget = EmptyStr) then
begin
// One side doesn't expose the link target (e.g. WFX/SFTP plugin).
// SFTP file size for a symlink equals the byte length of its target
// string, so equal sizes strongly imply equal targets. This is a
// best-effort check for the copy-then-verify use case.
Result := FFileL.Size = FFileR.Size;
Exit;
end;
// Also accept different textual forms that resolve to the same target.
LeftResolved := GetAbsoluteFileName(FFileL.Path, LeftTarget);
RightResolved := GetAbsoluteFileName(FFileR.Path, RightTarget);
Result := mbCompareFileNames(LeftResolved, RightResolved);
end;
begin
FState := srsNotEq;
if Assigned(FFileR) and not Assigned(FFileL) then
@ -552,7 +599,8 @@ begin
FState := srsCopyRight
else begin
FileTimeDiff := FileTimeCompare(FFileL.ModificationTime, FFileR.ModificationTime, FForm.FNtfsShift);
if ((FileTimeDiff = 0) or ignoreDate) and (FFileL.Size = FFileR.Size) then
if (((FileTimeDiff = 0) or ignoreDate) and (FFileL.Size = FFileR.Size))
or AreEquivalentLinks then
FState := srsEqual
else
if not ignoreDate then
@ -562,11 +610,18 @@ begin
if FileTimeDiff < 0 then
FState := srsCopyLeft;
end;
if FForm.chkAsymmetric.Checked and (FState = srsCopyLeft) then
FAction := srsDoNothing
else begin
if FForm.chkAsymmetric.Checked then
begin
// In asymmetric/mirror mode left is unconditionally authoritative.
// srsCopyLeft means right is newer — left still wins (overwrite right).
// srsNotEq means ambiguous diff (e.g. content check) — left still wins.
if FState in [srsCopyLeft, srsNotEq] then
FAction := srsCopyRight
else
FAction := FState;
end
else
FAction := FState;
end;
end;
{ TfrmSyncDirsDlg }