Before for this simple technical playground blog website, I always use github actions to trigger a full build for dynamic and static posts to deploy to IIS. Later on, I moved to linux docker, so everything is bundled into a docker image even for static posts.
I know there are many command line tools to sync files with ssh for linux, mac, or windows. But all of them are quite different. I also want to use a managed SSH package to do this simple stuff so I can learn something.
I tried to use SSH.NET with fsharp script, so after I created my static posts, I can run the script, something like:
dotnet fsi sync.fsx -- -p sync-posts --host xxx --user xxx
Source of the script
#r "nuget: SSH.NET"
#r "nuget: Fun.Build"
open Fun.Build
open Fun.Result
open Renci.SshNet
open System
open System.IO
open Spectre.Console
let locker = obj ()
let printfn fmt = Printf.kprintf (fun s -> lock locker (fun () -> System.Console.WriteLine s)) fmt
let localDir = xxx
let remoteDir = xxx
pipeline "sync-posts" {
description "Sync changed files from the local directory to the remote server via SFTP."
whenCmdArg "--host"
whenCmdArg "--user"
stage "sync" {
run (fun ctx -> async {
let host = ctx.GetCmdArg "--host"
let user = ctx.GetCmdArg "--user"
let password = AnsiConsole.Prompt(TextPrompt<string>("Enter SSH password: ").Secret())
let processFiles i files = async {
printfn "[%d] Connect to %s..." i host
use sftp = new SftpClient(host, user, password)
sftp.Connect()
printfn "[%d] Connected to %s as %s" i host user
let rec ensureDir (dir: string) =
if Path.GetDirectoryName dir |> sftp.Exists |> not then
ensureDir (Path.GetDirectoryName dir)
if not (sftp.Exists dir) then sftp.CreateDirectory(dir)
for file in files do
try
let localLastWriteTime = FileInfo(file).LastWriteTimeUtc
let remoteFile = Path.Combine(remoteDir, Path.GetRelativePath(localDir, file)).Replace("\\", "/")
let upload () =
ensureDir (Path.GetDirectoryName remoteFile)
use fileStream = File.OpenRead(file)
sftp.UploadFile(fileStream, remoteFile, true)
sftp.SetLastWriteTimeUtc(remoteFile, localLastWriteTime)
printfn "[%d] File uploaded: %s" i remoteFile
if sftp.Exists remoteFile then
let remoteLastWriteTime = sftp.GetLastWriteTimeUtc(remoteFile)
if localLastWriteTime <> remoteLastWriteTime then
upload ()
else
printfn "[%d] File is up to date: %s" i file
else
upload ()
with ex ->
printfn "[%d] Error sync file %s: %s" i file ex.Message
AnsiConsole.WriteException ex
}
do!
Directory.GetFiles(localDir, "*.*", SearchOption.AllDirectories)
|> Seq.splitInto (Math.Min(8, Environment.ProcessorCount))
|> Seq.mapi processFiles
|> Async.Parallel
|> Async.map ignore
printfn "Connect to %s..." host
use ssh = new SshClient(host, user, password)
ssh.Connect()
printfn "Restart the slaveoftime.site container..."
use cmd = ssh.RunCommand("cd /root/aliyun-sites-host && docker compose restart slaveoftime.site")
do! cmd.ExecuteAsync() |> Async.AwaitTask
printfn "Restarted slaveoftime.site container: %s" cmd.Result
})
}
runIfOnlySpecified
}
tryPrintPipelineCommandHelp ()