Skip to main content

How to Automate FTP File Uploads in Batch Script

Automating file uploads to an FTP (File Transfer Protocol) server is a classic system administration task. Whether you are pushing a nightly database backup to an off-site server or updating a website's static assets, manual uploads are inefficient and prone to human error. While modern Windows includes curl, many legacy environments still rely on the built-in ftp.exe client. By creating a script file (a set of instructions for the FTP client), you can perform fully automated, non-interactive uploads.

This guide will explain how to build robust FTP upload scripts using both the native ftp.exe client and curl.

Method 1: The Script File Pattern (Native FTP)

The native ftp.exe in Windows cannot accept commands interactively from a Batch FOR loop or pipe. Instead, you write your FTP commands to a temporary text file and tell the FTP client to read from it using the -s: flag. The FTP commands themselves (open, cd, binary, put, quit) are protocol-level commands that are language-independent, meaning they work the same regardless of the Windows display language.

@echo off
setlocal

set "Server=ftp.example.com"
set "User=myusername"
set "Pass=mypassword"
set "LocalFile=%~dp0backup.zip"
set "RemoteFolder=/backups"
set "LogFile=%~dp0ftp_upload.log"

rem --- Verify the local file exists before attempting upload ---
if not exist "%LocalFile%" (
echo [ERROR] Local file not found: %LocalFile%
endlocal
pause
exit /b 1
)

rem --- Build the FTP command script ---
rem --- This temporary file contains the sequence of FTP commands ---
set "FtpScript=%TEMP%\ftp_upload_%RANDOM%.ftp"

(
echo open %Server%
echo %User%
echo %Pass%
echo cd %RemoteFolder%
echo binary
echo put "%LocalFile%"
echo dir
echo quit
) > "%FtpScript%"

rem --- Execute the FTP client with the script file ---
echo [ACTION] Uploading "%LocalFile%" to %Server%:%RemoteFolder%...

ftp -s:"%FtpScript%" > "%LogFile%" 2>&1

set "FtpResult=%errorlevel%"

rem --- CRITICAL: Delete the script file immediately ---
rem --- It contains the password in plain text ---
if exist "%FtpScript%" del "%FtpScript%" >nul 2>&1

rem --- Check the result ---
if %FtpResult% neq 0 (
echo [ERROR] FTP client returned error code %FtpResult%.
echo [INFO] Check the log file for details: %LogFile%
endlocal
pause
exit /b 1
)

rem --- Check the log for common FTP error indicators ---
rem --- FTP error codes (like "530 Login failed" or "550 File not found") ---
rem --- start with 4xx or 5xx and are protocol-level (language-independent) ---
findstr /r /c:"^530 " /c:"^550 " /c:"^553 " /c:"^425 " /c:"^426 " "%LogFile%" >nul 2>&1
if %errorlevel% equ 0 (
echo [ERROR] FTP server reported an error. Check log: %LogFile%
echo.
echo [LOG] Relevant error lines:
findstr /r /c:"^5[0-9][0-9] " /c:"^4[0-9][0-9] " "%LogFile%"
endlocal
pause
exit /b 1
)

echo [SUCCESS] Upload completed.
echo [INFO] Transfer log saved to: %LogFile%

endlocal
pause

How the script file works:

  1. open %Server%: Connects to the FTP server.
  2. The username and password are sent on separate lines (the FTP client prompts for them in sequence).
  3. cd %RemoteFolder%: Changes to the target directory on the server.
  4. binary: Switches to binary transfer mode (essential for non-text files).
  5. put "%LocalFile%": Uploads the file.
  6. dir: Lists the remote directory contents (captured in the log for verification).
  7. quit: Disconnects and exits the FTP client.

FTP error code checking: Instead of parsing English error messages, the script searches for FTP protocol response codes (3-digit numbers at the start of lines). These codes are defined by the FTP protocol standard (RFC 959) and are the same worldwide:

Code RangeMeaning
2xxSuccess
3xxIntermediate (e.g., awaiting more input)
4xxTemporary failure (try again later)
5xxPermanent failure (bad credentials, file not found, permission denied)
warning

Password in plain text: The temporary FTP script file contains your password in clear text. The script deletes it immediately after use, but there is a brief window during which the file exists on disk. For sensitive environments, use curl with FTPS (Method 2) or SSH-based transfers instead.

If you have Windows 10 (version 1803+) or Windows 11, curl.exe is a significantly better option. It does not require a temporary script file, supports FTPS (FTP over SSL/TLS), handles passive mode automatically, and provides clear exit codes.

@echo off
setlocal enabledelayedexpansion

set "Server=ftp.example.com"
set "RemotePath=/backups/backup.zip"
set "LocalFile=%~dp0backup.zip"
set "User=myusername"
set "Pass=mypassword"
set "Timeout=30"

rem --- Verify curl is available ---
where curl.exe >nul 2>&1
if !errorlevel! neq 0 (
echo [ERROR] curl.exe is not available on this system.
echo [INFO] curl is included in Windows 10 version 1803 and later.
echo [INFO] For older systems, use Method 1 ^(native ftp.exe^).
endlocal
pause
exit /b 1
)

rem --- Verify the local file exists ---
if not exist "%LocalFile%" (
echo [ERROR] Local file not found: %LocalFile%
endlocal
pause
exit /b 1
)

rem --- Display file info ---
for %%f in ("%LocalFile%") do (
echo [INFO] File: %%~nxf
echo [INFO] Size: %%~zf bytes
)

echo [ACTION] Uploading to ftp://%Server%%RemotePath%...

rem --- Upload using curl ---
rem -T = Upload ^(Transfer^) the local file
rem --user = Credentials ^(user:password^)
rem --connect-timeout = Fail quickly if server is unreachable
rem --ftp-create-dirs = Create remote directories if they don't exist
curl.exe -T "%LocalFile%" --user "%User%:%Pass%" --connect-timeout %Timeout% --ftp-create-dirs "ftp://%Server%%RemotePath%" 2>&1

if !errorlevel! neq 0 (
echo.
echo [ERROR] Upload failed. curl exit code: !errorlevel!
echo [INFO] Common causes:
echo - Server unreachable ^(check hostname and network^)
echo - Invalid credentials ^(530 error^)
echo - Permission denied on remote directory ^(553 error^)
echo - Firewall blocking FTP data connections
endlocal
pause
exit /b 1
)

echo.
echo [SUCCESS] Upload complete.

endlocal
pause

Using FTPS (FTP over TLS) with curl

Standard FTP sends everything (including credentials) in plain text. FTPS encrypts the connection using SSL/TLS. curl supports this natively.

@echo off
setlocal enabledelayedexpansion

set "Server=ftp.example.com"
set "RemotePath=/backups/backup.zip"
set "LocalFile=%~dp0backup.zip"
set "User=myusername"
set "Pass=mypassword"

rem --- Verify curl and local file ---
where curl.exe >nul 2>&1
if !errorlevel! neq 0 (
echo [ERROR] curl not found.
endlocal
pause
exit /b 1
)

if not exist "%LocalFile%" (
echo [ERROR] File not found: %LocalFile%
endlocal
pause
exit /b 1
)

echo [ACTION] Uploading via FTPS ^(encrypted^)...

rem --- Use ftps:// protocol for explicit TLS ---
rem --ssl-reqd = Require SSL/TLS for both control and data connections
rem -k = Allow self-signed certificates ^(remove for strict validation^)
curl.exe -T "%LocalFile%" --user "%User%:%Pass%" --ssl-reqd --connect-timeout 30 "ftps://%Server%%RemotePath%" 2>&1

if !errorlevel! equ 0 (
echo [SUCCESS] Secure upload complete.
) else (
echo [ERROR] FTPS upload failed. Exit code: !errorlevel!
echo [INFO] The server may not support TLS. Try plain ftp:// as fallback.
)

endlocal
pause

Method 3: Uploading Multiple Files

When you need to upload several files (e.g., a directory of log files or daily reports), you can loop through matching files.

Using Native FTP

@echo off
setlocal enabledelayedexpansion

set "Server=ftp.example.com"
set "User=myusername"
set "Pass=mypassword"
set "SourceDir=%~dp0reports"
set "RemoteFolder=/uploads/reports"
set "LogFile=%~dp0ftp_batch_upload.log"

rem --- Verify source directory exists ---
if not exist "%SourceDir%\" (
echo [ERROR] Source directory not found: %SourceDir%
endlocal
pause
exit /b 1
)

rem --- Check if there are files to upload ---
set "HasFiles=0"
for %%f in ("%SourceDir%\*.csv") do set "HasFiles=1"

if "!HasFiles!"=="0" (
echo [INFO] No CSV files found in %SourceDir%. Nothing to upload.
endlocal
pause
exit /b 0
)

rem --- Build the FTP script with all files ---
set "FtpScript=%TEMP%\ftp_batch_%RANDOM%.ftp"

(
echo open %Server%
echo %User%
echo %Pass%
echo cd %RemoteFolder%
echo binary
for %%f in ("%SourceDir%\*.csv") do (
echo put "%%f"
)
echo dir
echo quit
) > "%FtpScript%"

echo [ACTION] Uploading CSV files from %SourceDir%...

ftp -s:"%FtpScript%" > "%LogFile%" 2>&1

rem --- Delete the script file immediately (contains password) ---
if exist "%FtpScript%" del "%FtpScript%" >nul 2>&1

rem --- Check for FTP protocol errors ---
findstr /r /c:"^5[0-9][0-9] " /c:"^4[0-9][0-9] " "%LogFile%" >nul 2>&1
if %errorlevel% equ 0 (
echo [WARN] Some files may have failed. Check log: %LogFile%
findstr /r /c:"^5[0-9][0-9] " /c:"^4[0-9][0-9] " "%LogFile%"
) else (
echo [SUCCESS] All files uploaded.
)

echo [INFO] Log saved to: %LogFile%

endlocal
pause

Using CURL (per-file with error tracking)

@echo off
setlocal enabledelayedexpansion

set "Server=ftp.example.com"
set "User=myusername"
set "Pass=mypassword"
set "SourceDir=%~dp0reports"
set "RemoteFolder=/uploads/reports"
set "LogFile=%~dp0ftp_batch_upload.log"

rem --- Verify source directory exists ---
if not exist "%SourceDir%\" (
echo [ERROR] Source directory not found: %SourceDir%
endlocal
pause
exit /b 1
)

rem --- Check if there are files to upload ---
set "HasFiles=0"
for %%f in ("%SourceDir%\*.csv") do set "HasFiles=1"

if "!HasFiles!"=="0" (
echo [INFO] No CSV files found in %SourceDir%. Nothing to upload.
endlocal
pause
exit /b 0
)

rem --- Build the FTP script with all files ---
set "FtpScript=%TEMP%\ftp_batch_%RANDOM%.ftp"

(
echo open %Server%
echo %User%
echo %Pass%
echo cd %RemoteFolder%
echo binary
for %%f in ("%SourceDir%\*.csv") do (
echo put "%%f"
)
echo dir
echo quit
) > "%FtpScript%"

echo [ACTION] Uploading CSV files from %SourceDir%...

ftp -s:"%FtpScript%" > "%LogFile%" 2>&1

rem --- Delete the script file immediately ^(contains password^) ---
if exist "%FtpScript%" del "%FtpScript%" >nul 2>&1

rem --- Check for FTP protocol errors ---
findstr /r /c:"^5[0-9][0-9] " /c:"^4[0-9][0-9] " "%LogFile%" >nul 2>&1
if !errorlevel! equ 0 (
echo [WARN] Some files may have failed. Check log: %LogFile%
findstr /r /c:"^5[0-9][0-9] " /c:"^4[0-9][0-9] " "%LogFile%"
) else (
echo [SUCCESS] All files uploaded.
)

echo [INFO] Log saved to: %LogFile%

endlocal
pause

How to Avoid Common Errors

Wrong Way: Uploading Binary Files in ASCII Mode

By default, some FTP clients use ASCII transfer mode. If you upload a .zip, .pdf, .exe, or any non-text file in ASCII mode, the file will be corrupted on the server. Characters like line endings get translated, breaking the file's binary structure.

rem *** BAD: missing "binary" command; defaults to ASCII ***
echo open ftp.example.com > script.ftp
echo user >> script.ftp
echo pass >> script.ftp
echo put backup.zip >> script.ftp
echo quit >> script.ftp

Correct Way: Always include the binary command before any put command when uploading non-text files.

rem *** GOOD: binary mode preserves file integrity ***
echo binary >> script.ftp
echo put backup.zip >> script.ftp

Wrong Way: Leaving the Script File on Disk

The FTP script file contains your username and password in plain text. Leaving it on disk is a security risk.

rem *** BAD: password file left behind ***
ftp -s:"upload.ftp"
rem (script exits without deleting upload.ftp)

Correct Way: Delete the script file immediately after use. Save the errorlevel first, then delete, then check the result.

ftp -s:"%FtpScript%"
set "Result=%errorlevel%"
del "%FtpScript%" >nul 2>&1
if %Result% neq 0 echo Upload failed.

Wrong Way: Trusting errorlevel Alone

The native ftp.exe client often returns errorlevel 0 even when individual file transfers fail (e.g., 550 Permission denied). It only returns non-zero for catastrophic failures like being unable to connect at all.

Correct Way: Log the FTP output and search for FTP protocol error codes (4xx and 5xx responses) in the log.

ftp -s:"%FtpScript%" > "%LogFile%" 2>&1
findstr /r /c:"^5[0-9][0-9] " /c:"^4[0-9][0-9] " "%LogFile%" >nul 2>&1
if %errorlevel% equ 0 echo [ERROR] FTP server reported errors.

Wrong Way: Embedding Credentials in Scripts for Production

Both the native FTP script file and the curl command line contain credentials in plain text. Anyone who can read the script or view the process list can see the password.

Correct Way: For production environments, consider:

  • Using .netrc files (curl supports --netrc) with restricted file permissions.
  • Using Windows Credential Manager via PowerShell.
  • Using SSH key-based authentication with SFTP instead of FTP.
  • At minimum, use FTPS (curl --ssl-reqd) to encrypt the credentials in transit.

Best Practices and Rules

1. Always Use Binary Mode for Non-Text Files

Include the binary command in your FTP script before any put command that uploads non-text data. ASCII mode is only appropriate for plain .txt or .csv files, and even then, binary mode works safely for text files too.

2. Always Delete Temporary Script Files

The FTP script file contains clear-text credentials. Delete it immediately after the ftp command finishes, before any error handling or logging that might cause the script to exit early.

3. Log and Verify Every Upload

Redirect FTP output to a log file and check for 4xx/5xx protocol error codes. Include a dir command at the end of your FTP script to capture the remote directory listing, confirming the file arrived.

4. FTP Protocol Commands Are Language-Independent

The FTP commands (open, cd, binary, put, get, dir, quit) and server response codes (like 230 Login successful, 550 File not found) are defined by RFC 959 and are the same in every language. The ftp.exe client's own status messages may be translated, but the protocol-level numbers are universal and safe to parse.

5. Prefer FTPS or SFTP Over Plain FTP

Standard FTP sends credentials and data in plain text. Use FTPS (curl --ssl-reqd with ftps://) for encrypted transfers. For SSH-based transfers, use the scp command or a tool like WinSCP. The native Windows ftp.exe does not support any form of encryption.

6. Handle Passive Mode Issues

If uploads hang at the data connection stage, the server may require passive mode. The native ftp.exe has limited passive mode support. curl handles passive mode automatically and reliably.

7. Use setlocal / endlocal

Always wrap scripts in setlocal and endlocal to prevent variables (especially those containing credentials) from leaking into the parent environment.

Final Thoughts

Automating FTP uploads transforms manual file management into a reliable, repeatable process. The native ftp.exe client works on every Windows version but requires a temporary script file (which contains plain-text credentials and must be deleted immediately after use) and has unreliable exit codes that require log-based error detection. curl.exe on modern Windows is the superior option, offering no temporary files, proper exit codes, FTPS encryption support, and automatic passive mode handling. Regardless of which tool you use, the essential practices are: always use binary mode for non-text files, always verify the upload by checking for FTP protocol error codes, always clean up credential-containing files, and whenever possible use encrypted transfer protocols instead of plain FTP.