Lazy Mode: Automating a Daily Backup and Database Restore

Photo by Cash Macanaya on Unsplash

I call this one Lazy Mode.

Not lazy in a bad way. I mean lazy in the good IT way: if something has to be done the same way every day, a script should probably be doing it.

This started with a daily backup and restore process.

The task was simple: download the latest database backup file from a secure backup location, save it locally, and restore it to a local database server. The work was not hard, but it had enough steps that doing it by hand every day could lead to mistakes.

So I built a PowerShell script for it.

The script checks the backup location, finds the latest database backup file, downloads it, verifies it, restores the database, runs post-restore updates, logs the result, and cleans up the temporary local copy.

The main rule is simple: use the latest backup only.

If the latest backup is locked or not ready, the script does not use an older backup. It waits and tries the same latest file again.

I tested it last night, and it completed the full task without any issues. That was the confirmation I wanted. It found the latest backup, handled the process, restored the database, and finished without me stepping through it manually.

That is Lazy Mode doing what it should do.

A Quick Security Note

This post is cleaned up for public sharing.

I am not sharing real server names, database names, usernames, saved connection names, IP addresses, internal file paths, logs, screenshots, certificate details, vendor details, tool names, or credentials.

The examples below use placeholders.

A technical post should explain the idea without exposing the environment.

Why I Automated It

Manual backup and restore work is not hard, but it is repetitive. Repetitive work is where mistakes happen.

The process has several small steps:

  • Check the secure backup location
  • Find the latest database backup file
  • Make sure the file is ready
  • Download the file
  • Avoid incomplete downloads
  • Validate the backup before restore
  • Restore the database
  • Run post-restore updates
  • Save logs
  • Clean up the local backup copy
  • Run it again on schedule

None of those steps are exciting, but they matter.

The goal was to make the process run the same way every time.

Why PowerShell Works Well Here

This is the kind of work PowerShell is good at.

PowerShell can work with files, folders, scheduled jobs, command-line tools, database tools, logs, and background jobs. That makes it useful for real IT work where different systems need to work together.

I did not need to build a full application. I just needed a script that could put the steps in the right order and run them the same way each time.

In this workflow, PowerShell handles:

  • Running a secure file transfer tool
  • Reading the backup file list
  • Finding the latest backup
  • Checking the local file size
  • Watching download progress
  • Retrying if the latest file is locked
  • Creating a temporary database restore file
  • Running the database restore command
  • Showing restore status
  • Saving logs
  • Cleaning up temporary files
  • Running from a scheduled job

That is the useful part of PowerShell. It helps automate the regular work that sits between systems.

Keeping the Script Easy to Manage

The script starts with parameters so the main values are easy to change later.

param(
    [string]$TransferProfile = "[SavedSecureTransferProfile]",
    [string]$RemoteBackupPath = "/",
    [string]$LocalStagingPath = "C:\Path\To\Local\Staging",
    [int]$ProgressIntervalSeconds = 1,
    [int]$LockedFileRetrySeconds = 10,
    [string]$DatabaseServer = "[DATABASE_SERVER]",
    [string]$DatabaseName = "[TARGET_DATABASE]",
    [string]$DatabaseDataFilePath = "C:\Path\To\Database\DataFile",
    [string]$DatabaseLogFilePath = "C:\Path\To\Database\LogFile"
)

The script does not store the password in the code. The connection is handled through a saved setup that is protected on the server.

Clear Status Messages

Long-running scripts need clear messages. I used small helper functions for status, success, and failure messages.

function Write-Status {
    param([string]$Message)

    Write-Host ""
    Write-Host "[$(Get-Date -Format 'MM/dd/yyyy HH:mm:ss')] $Message" -ForegroundColor Cyan
}

function Write-Success {
    param([string]$Message)

    Write-Host "[$(Get-Date -Format 'MM/dd/yyyy HH:mm:ss')] $Message" -ForegroundColor Green
}

function Write-Failure {
    param([string]$Message)

    Write-Host "[$(Get-Date -Format 'MM/dd/yyyy HH:mm:ss')] $Message" -ForegroundColor Red
}

This makes troubleshooting easier. If something fails, I want to know where it failed.

Finding the Latest Backup

Instead of downloading a hard-coded file, the script lists the backup files and sorts them by date.

$SortedBackupFiles = @($BackupFiles | Sort-Object LastModified -Descending)
$LatestBackup = $SortedBackupFiles | Select-Object -First 1

That matters because the backup filename changes. The script should not need to be edited every day.

Skipping the Download if the File Already Exists

The script checks if the latest backup already exists locally. If it does, and the file size matches, it skips the download and starts the restore.

$LocalExistingFilePath = Join-Path $LocalStagingPath $LatestBackup.Name

if (Test-Path $LocalExistingFilePath) {
    $LocalFile = Get-Item $LocalExistingFilePath

    if ($LocalFile.Length -eq [long]$LatestBackup.Size) {
        Write-Success "Local file matches the latest backup."
        Restore-Database -BackupFilePath $LocalFile.FullName
        return $true
    }
}

This saves time and avoids downloading a large file again for no reason.

The file-size check is useful, but it is not the only check. A backup can have the right size and still need to be verified before restore.

Downloading Safely

The script downloads to a temporary file first.

$LocalTempFileName = "$($BackupFile.Name).download"
$LocalTempFilePath = Join-Path $LocalStagingPath $LocalTempFileName

Only after the download completes does the script rename it to the final backup file.

That way, an incomplete download does not look like a good backup.

Showing Download Progress

The script checks the file size while the download is running and prints progress every five percent.

while ($DownloadJob.State -eq "Running") {
    $CurrentBytes = [long]$PartialFile.Length
    $PercentComplete = [math]::Floor(($CurrentBytes / $ExpectedBytes) * 100)

    while ($PercentComplete -ge $NextProgressToShow -and $NextProgressToShow -lt 100) {
        Write-Host "Downloading: $NextProgressToShow% complete"
        $NextProgressToShow += 5
    }

    Start-Sleep -Seconds $ProgressIntervalSeconds
}

That gives enough feedback without filling the screen with too many lines.

Retrying the Latest Backup if It Is Locked

This was an important change.

At first, the script could try the next available backup if the latest file was locked. That worked, but it created a problem: the next backup might be old.

For this process, an old backup is not good enough.

So the script now only uses the latest backup. If the latest file is locked, it waits and tries again.

$DownloadSucceeded = $false
$RetryAttempt = 1

while ($DownloadSucceeded -eq $false) {

    Write-Status "Trying latest backup file only: $($LatestBackup.Name)"
    Write-Host "Attempt: $RetryAttempt"
    Write-Host "Older backup files will not be used."

    $Result = Download-BackupCandidate -BackupFile $LatestBackup

    if ($Result -eq $true) {
        $DownloadSucceeded = $true
        break
    }

    Write-Failure "Latest backup could not be downloaded."
    Write-Failure "It may still be locked or not ready."
    Write-Host "Waiting before trying the same latest file again..."

    Start-Sleep -Seconds $LockedFileRetrySeconds

    $RetryAttempt++
}

The goal is not just to restore any backup. The goal is to restore the latest backup.

Because this can keep waiting, the job also needs monitoring. If the latest backup stays locked too long, someone should know about it.

Validating and Restoring the Database

Before the actual restore, the script can run a backup validation step.

Validate backup file before restore.

This does not replace a real test restore, but it is a good extra check before restoring the database.

After the backup is ready, the script creates a temporary restore file and runs the database restore command.

$RestoreArgs = @(
    "-Server", $DatabaseServer,
    "-Database", $DatabaseName,
    "-InputFile", $TempRestoreFilePath,
    "-OutputFile", $RestoreOutputLog
)

The script uses an approved authentication method, so database credentials are not stored directly in the script.

The restore also reports progress while it is running.

Restore target database.
Move data file to approved local path.
Move log file to approved local path.
Replace existing local copy.
Report progress during restore.

The script writes restore output to a log file. That helps when running it manually, and it also keeps a record when it runs as a scheduled job.

Post-Restore Updates

After the database restore completes, the script can run post-restore updates. In some environments, that may include application settings that need to be adjusted after a restore.

For a public post, those values should stay sanitized.

Update local application settings.
Update local paths.
Update local service settings.
Use placeholder values in public examples.

The restore is not really done until the database is ready for the local environment.

Data in Motion and Data at Rest

The backup has to be protected while it is moving and while it is sitting on disk.

For data in motion, the backup should not be transferred in plain text. The transfer should use an encrypted method. The server identity should also be checked, either through a certificate or host key, so the script is connecting to the right system.

The server running the task should only be allowed to connect where it needs to connect. It should not become a general-purpose transfer box.

For data at rest, the downloaded backup file has to be treated like sensitive data. A backup file can contain a full copy of the database. That means the local folder should be restricted to only the service account and admins who need access.

My preferred rule is to treat the downloaded backup as a temporary working copy:

  • Download the latest backup
  • Restore the database
  • Run validation or post-restore checks
  • Delete the local backup file after a successful restore

If the local backup file has to be kept, then it should be protected:

  • Store it in a restricted folder
  • Encrypt the drive or file
  • Keep only what is needed
  • Delete it after the approved retention period

The point is simple: automation should not leave extra copies of sensitive data lying around.

Protecting Logs and Temporary Files

Logs are helpful, but they can also expose details.

A log file may show backup filenames, paths, server names, usernames, database names, or error messages. Temporary restore files can also contain restore paths and database names.

Because of that, logs and temporary files should be stored in a restricted folder. They should not be readable by everyone.

The script should also clean up temporary files when the job succeeds. For failed jobs, keeping the logs is useful, but they should still be protected and rotated.

Securing the Scheduled Job

The scheduled job should not become the weak point.

For this type of automation, I would not run it as my personal account or as a domain admin. A better setup is a dedicated service account, or a managed service account if the environment supports it.

The account should only have what it needs:

  • Access to the script
  • Access to the staging folder
  • Access to the log folder
  • Permission to run the file transfer tool
  • Permission to run the database restore command
  • Database rights needed to restore the target database

The script folder should be read-only for the service account. That way, the service account can run the script, but it cannot easily change the script.

The staging and log folders should be restricted too. The service account can have the access it needs, but regular users should not have access.

For the account itself, I would allow it to run scheduled jobs, but block normal interactive logins. It should not be used to log in to desktops or servers like a normal user.

The job should also be set to:

  • Run whether a user is logged on or not
  • Avoid starting a second copy if one is already running
  • Use a long enough time limit, or no short time limit, since the script may wait for a locked file
  • Write logs so the job can be reviewed later
  • Alert someone if the latest backup stays locked too long or the restore fails

That keeps the automation useful without making it loose.

The Assumptions Built Into the Code

Every script has assumptions. Some are obvious. Some only show up when something fails.

For this script, these are the assumptions:

  1. The latest backup is the only acceptable backup.
    If the latest backup file is locked, the script waits and retries. It does not use an older backup.
  2. The file size from the backup site is good enough to compare.
    If the local file exists and the size matches the remote file, the script treats it as complete and skips the download.
  3. The backup should still be validated before restore.
    File size is helpful, but backup validation adds another safety check.
  4. The backup files are in a known place.
    The script expects the backup files to be in a known remote folder.
  5. The secure file transfer tool is already installed and set up.
    The script depends on a saved connection setup instead of storing connection details in the code.
  6. The saved connection details are protected.
    The saved setup file should not be readable by everyone.
  7. The account running the script has only the access it needs.
    It needs access to the local staging folder, permission to run the file transfer tool, permission to run the database restore command, and enough database rights to restore the database.
  8. The staging folder is restricted.
    A database backup file can contain a full copy of the database, so it should not be accessible to everyone.
  9. The downloaded backup is temporary unless there is a reason to keep it.
    After a successful restore, the local downloaded backup should be deleted. If it must be kept, it should be encrypted, access-restricted, and retained only for the approved period.
  10. Data in motion must be protected.
    The backup should be transferred over an encrypted connection, and the server identity should be verified.
  11. Data at rest must be protected.
    The local staging folder, logs, and temporary files should be restricted. If the downloaded backup is kept, it should be encrypted.
  12. The database restore paths are correct.
    The data and log file paths must match the local server.
  13. The scheduled job has enough time to run.
    Since the script keeps waiting if the latest backup is locked, the job should not have a short timeout.
  14. The automation is monitored.
    If the latest backup stays locked or the restore fails, someone should know.
  15. The automation is approved for the environment.
    A script can work technically and still be wrong if it breaks policy, vendor rules, or security rules.
  16. The public explanation must stay sanitized.
    Real credentials, server names, customer data, protected information, logs, screenshots, internal paths, product names, and vendor details should not be posted publicly.
  17. The automation runs inside the normal controlled environment.
    The script does not create new access for people who should not have it. It runs inside the internal network and relies on the existing database, application, and permission setup.

These assumptions matter.

A script is not magic. It is just the process written down in code.

Compliance and Confidentiality

Before posting technical work publicly, I try to separate the lesson from the environment.

This post is about the pattern: using PowerShell to automate a backup download and database restore.

It is not about exposing a production environment.

The automation runs within the internal network. Access to the database, the application, and the restored data is limited to specific authorized users. What each person can see and do is still controlled by the database, the application, and the existing permission settings.

The script does not replace access control. It works inside the access controls that already exist.

That means no real credentials, no customer data, no protected information, no production screenshots, no internal logs, and no details that would help someone access or map the environment.

Whether the concern is privacy, cybersecurity policy, vendor confidentiality, or internal IT policy, the safest approach is the same: explain the workflow, but do not share sensitive details.

Scheduling Lazy Mode

The final step is scheduling the script to run daily.

$Trigger = New-ScheduledTaskTrigger `
    -Daily `
    -At "[ScheduledTime]"

The job should also be set to avoid starting a second copy if the first one is still running.

$Settings = New-ScheduledTaskSettingsSet `
    -StartWhenAvailable `
    -MultipleInstances IgnoreNew

That matters because large backups and database restores can take time.

One thing to remember: if the latest backup stays locked for a long time, this script will keep waiting and retrying. That is intentional. Since older backups should not be used, the scheduled job needs enough time to keep running until the latest file is ready.

Final Thoughts

Lazy Mode is not about avoiding responsibility. It is about removing repeated manual work.

This script does the same steps the same way every time:

  • Finds the latest backup
  • Skips unnecessary downloads
  • Avoids incomplete files
  • Protects data in motion
  • Protects data at rest
  • Validates the backup
  • Waits if the latest file is locked
  • Refuses to use an older backup
  • Restores the database
  • Runs post-restore updates
  • Protects logs and temporary files
  • Deletes or encrypts the downloaded backup after use
  • Runs on a schedule

That is practical automation.

It is not flashy. It is not overbuilt. It is just a script that does the boring work correctly.

And sometimes that is exactly what IT needs.

PowerShell makes this kind of work possible because it works where IT work actually happens: files, logs, scheduled jobs, command-line tools, and systems that need to work together.

That is the beauty of it.

Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *