Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion wfx/rclone/build/build_linux.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash
# Build rclone WFX plugin on Debian 12 (x86_64)
# Copyright (C) 2026 Miklos Mukka Szel <contact@miklos-szel.com>
# Copyright (C) 2026 Miklos Mukka Szel <hello@miklos-szel.com>
#
# Usage:
# sudo ./build_linux.sh Build the plugin (installs dependencies)
Expand Down
2 changes: 1 addition & 1 deletion wfx/rclone/build/build_macos.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash
# Build rclone WFX plugin on macOS using Free Pascal
# Copyright (C) 2026 Miklos Mukka Szel <contact@miklos-szel.com>
# Copyright (C) 2026 Miklos Mukka Szel <hello@miklos-szel.com>
#
# Usage:
# ./build_macos.sh Build the plugin
Expand Down
2 changes: 1 addition & 1 deletion wfx/rclone/build/build_windows.bat
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
setlocal enabledelayedexpansion

:: Build rclone WFX plugin on Windows
:: Copyright (C) 2026 Miklos Mukka Szel <contact@miklos-szel.com>
:: Copyright (C) 2026 Miklos Mukka Szel <hello@miklos-szel.com>
:: Requires Lazarus IDE installed
::
:: Usage:
Expand Down
2 changes: 1 addition & 1 deletion wfx/rclone/src/rclone.lpr
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
-------------------------------------------------------------------------
WFX plugin for working with rclone remotes

Copyright (C) 2026 Miklos Mukka Szel <contact@miklos-szel.com>
Copyright (C) 2026 Miklos Mukka Szel <hello@miklos-szel.com>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
Expand Down
102 changes: 62 additions & 40 deletions wfx/rclone/src/urclonecli.pas
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
-------------------------------------------------------------------------
WFX plugin for working with rclone remotes - CLI wrapper

Copyright (C) 2026 Miklos Mukka Szel <contact@miklos-szel.com>
Copyright (C) 2026 Miklos Mukka Szel <hello@miklos-szel.com>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
Expand All @@ -27,7 +27,7 @@
interface

uses
Classes, SysUtils, Process, WfxPlugin, uRcloneJson;
Classes, SysUtils, Process, Pipes, WfxPlugin, uRcloneJson;

type
TProgressCallback = function(SourceName, TargetName: PWideChar;
Expand All @@ -40,7 +40,7 @@ TRcloneCli = class
FProgressProc: TProgressCallback;
FPluginNr: Integer;
function RunCommand(const Args: array of AnsiString; out Output: AnsiString;
out ExitCode: Integer): Boolean;
out ErrorOutput: AnsiString; out ExitCode: Integer): Boolean;
function RunCommandWithProgress(const Args: array of AnsiString;
const SourceName, TargetName: UnicodeString;
out ExitCode: Integer): Boolean;
Expand Down Expand Up @@ -159,23 +159,44 @@ function GetRcloneConfigPath: AnsiString;
Result := GetHomeDir + '/.config/rclone/rclone.conf';
end;

{ Read everything currently buffered in a pipe without blocking.
Returns the number of bytes appended to AStream. }
function DrainPipe(APipe: TInputPipeStream; AStream: TStringStream): Integer;
var
Buffer: array[0..4095] of Byte;
Avail, BytesRead: Integer;
begin
Result := 0;
if APipe = nil then Exit;
repeat
Avail := APipe.NumBytesAvailable;
if Avail <= 0 then Break;
if Avail > SizeOf(Buffer) then
Avail := SizeOf(Buffer);
BytesRead := APipe.Read(Buffer, Avail);
if BytesRead <= 0 then Break;
AStream.Write(Buffer, BytesRead);
Inc(Result, BytesRead);
until False;
end;

function TRcloneCli.RunCommand(const Args: array of AnsiString;
out Output: AnsiString; out ExitCode: Integer): Boolean;
out Output: AnsiString; out ErrorOutput: AnsiString; out ExitCode: Integer): Boolean;
var
AProcess: TProcess;
I: Integer;
OutputStream: TStringStream;
BytesRead: Integer;
Buffer: array[0..4095] of Byte;
OutputStream, ErrorStream: TStringStream;
ConfigPath: AnsiString;
begin
Result := False;
Output := '';
ErrorOutput := '';
ExitCode := -1;
FLastError := '';

AProcess := TProcess.Create(nil);
OutputStream := TStringStream.Create('');
ErrorStream := TStringStream.Create('');
try
AProcess.Executable := FRclonePath;

Expand All @@ -194,30 +215,27 @@ function TRcloneCli.RunCommand(const Args: array of AnsiString;
AProcess.Environment.Add('PATH=/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin');
AProcess.Environment.Add('HOME=' + GetHomeDir);

AProcess.Options := [poUsePipes, poStderrToOutPut, poNoConsole];
// Keep stderr separate: rclone writes log messages (e.g. the SFTP
// "No host key validation is being performed" NOTICE) to stderr, and
// merging them into stdout would corrupt the JSON of lsjson & friends.
AProcess.Options := [poUsePipes, poNoConsole];

try
AProcess.Execute;

// Read output
while AProcess.Running or (AProcess.Output.NumBytesAvailable > 0) do
begin
BytesRead := AProcess.Output.Read(Buffer, SizeOf(Buffer));
if BytesRead > 0 then
OutputStream.Write(Buffer, BytesRead)
else
Sleep(10);
end;

// Read any remaining output
// Drain both pipes until the process is gone and nothing is left
repeat
BytesRead := AProcess.Output.Read(Buffer, SizeOf(Buffer));
if BytesRead > 0 then
OutputStream.Write(Buffer, BytesRead);
until BytesRead = 0;
if DrainPipe(AProcess.Output, OutputStream) +
DrainPipe(AProcess.Stderr, ErrorStream) = 0 then
begin
if not AProcess.Running then Break;
Sleep(10);
end;
until False;

ExitCode := AProcess.ExitCode;
Output := OutputStream.DataString;
ErrorOutput := ErrorStream.DataString;
Result := True;
except
on E: Exception do
Expand All @@ -227,6 +245,7 @@ function TRcloneCli.RunCommand(const Args: array of AnsiString;
end;
end;
finally
ErrorStream.Free;
OutputStream.Free;
AProcess.Free;
end;
Expand Down Expand Up @@ -378,27 +397,30 @@ function TRcloneCli.RunCommandWithProgress(const Args: array of AnsiString;

function TRcloneCli.ListRemotes: TStringList;
var
Output: AnsiString;
Output, ErrorOutput: AnsiString;
ExitCode: Integer;
begin
Result := nil;
if RunCommand(['listremotes'], Output, ExitCode) and (ExitCode = 0) then
if RunCommand(['listremotes'], Output, ErrorOutput, ExitCode) and (ExitCode = 0) then
Result := ParseListRemotes(Output)
else
begin
FLastError := ErrorOutput;
Result := TStringList.Create;
end;
end;

function TRcloneCli.ListDirectory(const RemotePath: AnsiString): TRcloneFileList;
var
Output: AnsiString;
Output, ErrorOutput: AnsiString;
ExitCode: Integer;
begin
Result := nil;
if RunCommand(['lsjson', RemotePath], Output, ExitCode) and (ExitCode = 0) then
if RunCommand(['lsjson', RemotePath], Output, ErrorOutput, ExitCode) and (ExitCode = 0) then
Result := ParseLsJson(Output)
else
begin
FLastError := Output;
FLastError := ErrorOutput;
Result := TRcloneFileList.Create(True);
end;
end;
Expand Down Expand Up @@ -441,42 +463,42 @@ function TRcloneCli.CopyToRemote(const LocalPath, RemotePath: AnsiString): Integ

function TRcloneCli.DeleteFile(const RemotePath: AnsiString): Boolean;
var
Output: AnsiString;
Output, ErrorOutput: AnsiString;
ExitCode: Integer;
begin
Result := RunCommand(['delete', RemotePath], Output, ExitCode) and (ExitCode = 0);
Result := RunCommand(['delete', RemotePath], Output, ErrorOutput, ExitCode) and (ExitCode = 0);
if not Result then
FLastError := Output;
FLastError := ErrorOutput;
end;

function TRcloneCli.DeleteDir(const RemotePath: AnsiString): Boolean;
var
Output: AnsiString;
Output, ErrorOutput: AnsiString;
ExitCode: Integer;
begin
Result := RunCommand(['rmdir', RemotePath], Output, ExitCode) and (ExitCode = 0);
Result := RunCommand(['rmdir', RemotePath], Output, ErrorOutput, ExitCode) and (ExitCode = 0);
if not Result then
FLastError := Output;
FLastError := ErrorOutput;
end;

function TRcloneCli.MakeDir(const RemotePath: AnsiString): Boolean;
var
Output: AnsiString;
Output, ErrorOutput: AnsiString;
ExitCode: Integer;
begin
Result := RunCommand(['mkdir', RemotePath], Output, ExitCode) and (ExitCode = 0);
Result := RunCommand(['mkdir', RemotePath], Output, ErrorOutput, ExitCode) and (ExitCode = 0);
if not Result then
FLastError := Output;
FLastError := ErrorOutput;
end;

function TRcloneCli.MoveFile(const OldPath, NewPath: AnsiString): Boolean;
var
Output: AnsiString;
Output, ErrorOutput: AnsiString;
ExitCode: Integer;
begin
Result := RunCommand(['moveto', OldPath, NewPath], Output, ExitCode) and (ExitCode = 0);
Result := RunCommand(['moveto', OldPath, NewPath], Output, ErrorOutput, ExitCode) and (ExitCode = 0);
if not Result then
FLastError := Output;
FLastError := ErrorOutput;
end;

function RcloneExitToWfx(ExitCode: Integer; const ErrorOutput: AnsiString): Integer;
Expand Down
2 changes: 1 addition & 1 deletion wfx/rclone/src/urclonefunc.pas
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
-------------------------------------------------------------------------
WFX plugin for working with rclone remotes

Copyright (C) 2026 Miklos Mukka Szel <contact@miklos-szel.com>
Copyright (C) 2026 Miklos Mukka Szel <hello@miklos-szel.com>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
Expand Down
30 changes: 21 additions & 9 deletions wfx/rclone/src/urclonejson.pas
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
-------------------------------------------------------------------------
WFX plugin for working with rclone remotes - JSON parsing

Copyright (C) 2026 Miklos Mukka Szel <contact@miklos-szel.com>
Copyright (C) 2026 Miklos Mukka Szel <hello@miklos-szel.com>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
Expand Down Expand Up @@ -67,16 +67,27 @@ function ParseLsJson(const JsonOutput: AnsiString): TRcloneFileList;
JsonData: TJSONData;
JsonArray: TJSONArray;
JsonObj: TJSONObject;
I: Integer;
I, StartPos: Integer;
RcloneFile: TRcloneFile;
Text: AnsiString;
begin
Result := TRcloneFileList.Create(True);

if Trim(JsonOutput) = '' then
Text := Trim(JsonOutput);
if Text = '' then
Exit;

// Be tolerant of anything printed before the array (log lines, warnings)
if Text[1] <> '[' then
begin
StartPos := Pos('[', Text);
if StartPos = 0 then
Exit;
Text := Copy(Text, StartPos, Length(Text) - StartPos + 1);
end;

try
JsonData := GetJSON(JsonOutput);
JsonData := GetJSON(Text);
try
if not (JsonData is TJSONArray) then
Exit;
Expand Down Expand Up @@ -134,11 +145,12 @@ function ParseListRemotes(const Output: AnsiString): TStringList;
for I := 0 to Lines.Count - 1 do
begin
RemoteName := Trim(Lines[I]);
// rclone listremotes outputs "remotename:" - remove the trailing colon
if (Length(RemoteName) > 0) and (RemoteName[Length(RemoteName)] = ':') then
RemoteName := Copy(RemoteName, 1, Length(RemoteName) - 1);
if RemoteName <> '' then
Result.Add(RemoteName);
// rclone listremotes outputs "remotename:" - anything without the
// trailing colon is not a remote (e.g. a stray log line)
if (Length(RemoteName) < 2) or (RemoteName[Length(RemoteName)] <> ':') then
Continue;
RemoteName := Copy(RemoteName, 1, Length(RemoteName) - 1);
Result.Add(RemoteName);
end;
finally
Lines.Free;
Expand Down
2 changes: 1 addition & 1 deletion wfx/rclone/src/urcloneutil.pas
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
-------------------------------------------------------------------------
WFX plugin for working with rclone remotes

Copyright (C) 2026 Miklos Mukka Szel <contact@miklos-szel.com>
Copyright (C) 2026 Miklos Mukka Szel <hello@miklos-szel.com>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
Expand Down