mirror of
https://github.com/doublecmd/doublecmd.git
synced 2026-06-21 09:58:13 +00:00
FTP plugin: preserve Unix file permissions on transfer
On Linux/Unix, file permissions are meaningful (scripts, configs). This change
makes the FTP plugin carry those permissions across upload and download so that
a round-trip does not silently drop them.
Upload (StoreFile):
Read the local file's permission bits with FpStat, then issue
SITE CHMOD <mode> <path>. Supported by vsftpd, proftpd, pure-ftpd and
most other Unix-targeted FTP daemons. The FPC Format() '%o' specifier is
not implemented; DecToOct() from DCStrUtils is used instead.
Download (RetrieveFile):
After a successful download, call GetRemoteMode() to fetch the remote
file's permissions via MLST unix.mode, then apply them with FpChmod().
New helpers:
ParseOctalMode() — strips optional '0o'/'0O' prefix (pyftpdlib and
some other servers return Python-style octals) and
converts to a TFileAttrs value; returns 0 on any
parse error so a bad server response never crashes.
ExtractMlstFactValue() — pulls one named fact value out of an MLST/MLSD
response line; case-insensitive per RFC 3659 §7.5.
GetRemoteMode() — sends MLST, finds the unix.mode fact and returns
the parsed value; skips the round-trip entirely
when the server didn't advertise MLST in FEAT.
OPTS MLST (Login):
On Unix builds, request unix.mode together with the facts already requested
(unix.owner, unix.group, type, size, modify, perm, unique) so that servers
that provide those facts by default are not unnecessarily restricted.
Guarded with {$IFDEF UNIX} — Windows builds have no FpChmod, so there is
no point asking for unix.mode there. RFC 3659 says servers silently ignore
unknown fact names, so adding extra names is safe against all conforming
servers.
All permission code is inside {$IFDEF UNIX} blocks. Every failure path
(server returns non-2xx to MLST, SITE CHMOD refused, zero parsed mode) either
returns False or is silently ignored, so existing users on servers that do not
support these extensions see no change in behaviour.
This commit is contained in:
parent
45b0cafb62
commit
6d11206688
1 changed files with 87 additions and 1 deletions
|
|
@ -127,6 +127,7 @@ type
|
|||
function FileProperties(const FileName: String): Boolean; virtual;
|
||||
function CopyFile(const OldName, NewName: String): Boolean; virtual;
|
||||
function ChangeMode(const FileName, Mode: String): Boolean; virtual;
|
||||
function GetRemoteMode(const FileName: String; out Mode: TFileAttrs): Boolean; virtual;
|
||||
function List(Directory: String; NameList: Boolean): Boolean; override;
|
||||
function StoreFile(const FileName: string; Restore: Boolean): Boolean; override;
|
||||
function ExecuteCommand(const Command: String; const Directory: String = ''): Boolean; virtual;
|
||||
|
|
@ -153,6 +154,9 @@ uses
|
|||
DCDateTimeUtils
|
||||
{$IF (FPC_FULLVERSION < 30000)}
|
||||
, LazUTF8SysUtils
|
||||
{$ENDIF}
|
||||
{$IFDEF UNIX}
|
||||
, BaseUnix
|
||||
{$ENDIF}
|
||||
;
|
||||
|
||||
|
|
@ -455,6 +459,41 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
{ Strip optional '0o'/'0O' prefix (pyftpdlib quirk) then parse as octal.
|
||||
Returns 0 on any parse error so a bad server response never crashes DC. }
|
||||
function ParseOctalMode(const Value: String): TFileAttrs;
|
||||
var
|
||||
S: String;
|
||||
begin
|
||||
S := Value;
|
||||
if (Length(S) >= 2) and (S[1] = '0') and ((S[2] = 'o') or (S[2] = 'O')) then
|
||||
S := Copy(S, 3, MaxInt);
|
||||
try
|
||||
Result := OctToDec(S);
|
||||
except
|
||||
Result := 0;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Extract the raw value of a named fact from one MLST/MLSD response line.
|
||||
Fact names are matched case-insensitively (RFC 3659 §7.5).
|
||||
Returns the trimmed value string, or '' when the fact is absent in Line. }
|
||||
function ExtractMlstFactValue(const Line, FactName: String): String;
|
||||
var
|
||||
Key: String;
|
||||
Idx, Semi: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
Key := LowerCase(FactName) + '=';
|
||||
Idx := Pos(Key, LowerCase(Line));
|
||||
if Idx = 0 then Exit;
|
||||
Result := Copy(Line, Idx + Length(Key), MaxInt);
|
||||
Semi := Pos(';', Result);
|
||||
if Semi > 0 then
|
||||
Result := Copy(Result, 1, Semi - 1);
|
||||
Result := Trim(Result);
|
||||
end;
|
||||
|
||||
function TFTPSendEx.ListMachine(Directory: String): Boolean;
|
||||
var
|
||||
v: String;
|
||||
|
|
@ -537,7 +576,7 @@ begin
|
|||
end
|
||||
else if (option = 'unix.mode') then
|
||||
begin
|
||||
flr.Mode:= flr.Mode or OctToDec(value);
|
||||
flr.Mode:= flr.Mode or ParseOctalMode(value);
|
||||
end;
|
||||
if (y < Length(v)) and (v[y + 1] = ' ') then
|
||||
begin
|
||||
|
|
@ -720,6 +759,14 @@ begin
|
|||
ConvertFromUtf8:= @Ymmud;
|
||||
FTPCommand('OPTS UTF8 ON');
|
||||
end;
|
||||
{$IFDEF UNIX}
|
||||
// Tell the server which MLST facts we want. unix.mode is required for
|
||||
// permission preservation; the rest are kept so that real servers
|
||||
// (vsftpd, proftpd) don't lose facts they already return by default.
|
||||
// Per RFC 3659, any fact name the server doesn't know is silently ignored.
|
||||
if FMachine then
|
||||
FTPCommand('OPTS MLST unix.mode;unix.owner;unix.group;type;size;modify;perm;unique;');
|
||||
{$ENDIF}
|
||||
end;
|
||||
if (not FMachine) and FShowHidden then
|
||||
begin
|
||||
|
|
@ -864,11 +911,36 @@ begin
|
|||
Result:= FTPCommand('MFMT ' + Time + ' ' + FileName) = 213;
|
||||
end;
|
||||
|
||||
{ Query MLST for a remote file and return its Unix permission bits in Mode.
|
||||
Returns False when the server doesn't support MLST, when the response
|
||||
contains no unix.mode fact, or when the parsed value is zero. }
|
||||
function TFTPSendEx.GetRemoteMode(const FileName: String; out Mode: TFileAttrs): Boolean;
|
||||
var
|
||||
I: Integer;
|
||||
RawVal: String;
|
||||
begin
|
||||
Result := False;
|
||||
Mode := 0;
|
||||
if not FMachine then Exit; // Server didn't advertise MLST in FEAT — skip round-trip
|
||||
if (FTPCommand('MLST ' + FileName) div 100) <> 2 then Exit;
|
||||
for I := 0 to FullResult.Count - 1 do
|
||||
begin
|
||||
RawVal := ExtractMlstFactValue(FullResult[I], 'unix.mode');
|
||||
if RawVal = '' then Continue;
|
||||
Mode := ParseOctalMode(RawVal); // handles '0o' prefix and parse errors
|
||||
Result := Mode <> 0;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TFTPSendEx.StoreFile(const FileName: string; Restore: Boolean): Boolean;
|
||||
var
|
||||
StorSize: Int64;
|
||||
RestoreAt: Int64 = 0;
|
||||
SendStream: TProgressStream;
|
||||
{$IFDEF UNIX}
|
||||
LocalStat: BaseUnix.TStat;
|
||||
{$ENDIF}
|
||||
begin
|
||||
Result := False;
|
||||
Restore := Restore and FCanResume;
|
||||
|
|
@ -909,6 +981,12 @@ begin
|
|||
if (FTPCommand('STOR ' + FileName) div 100) <> 1 then
|
||||
Exit;
|
||||
Result := DataWrite(SendStream);
|
||||
{$IFDEF UNIX}
|
||||
if Result then
|
||||
if FpStat(FDirectFileName, LocalStat) = 0 then
|
||||
// FPC's Format() does not support %o; use DecToOct from DCStrUtils.
|
||||
ChangeMode(FileName, DecToOct(LocalStat.st_mode and $0FFF));
|
||||
{$ENDIF}
|
||||
finally
|
||||
SendStream.Free;
|
||||
end;
|
||||
|
|
@ -917,6 +995,9 @@ end;
|
|||
function TFTPSendEx.RetrieveFile(const FileName: string; FileSize: Int64; Restore: Boolean): Boolean;
|
||||
var
|
||||
RetrStream: TProgressStream;
|
||||
{$IFDEF UNIX}
|
||||
RemoteMode: TFileAttrs;
|
||||
{$ENDIF}
|
||||
begin
|
||||
Result := False;
|
||||
if not DataSocket then Exit;
|
||||
|
|
@ -947,6 +1028,11 @@ begin
|
|||
if (FTPCommand('RETR ' + FileName) div 100) <> 1 then
|
||||
Exit;
|
||||
Result := DataRead(RetrStream);
|
||||
{$IFDEF UNIX}
|
||||
if Result then
|
||||
if GetRemoteMode(FileName, RemoteMode) then
|
||||
FpChmod(FDirectFileName, RemoteMode and $0FFF);
|
||||
{$ENDIF}
|
||||
finally
|
||||
RetrStream.Free;
|
||||
end;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue