Initial commit

This commit is contained in:
over-engineer
2022-10-21 15:28:42 +03:00
commit febde54164
8 changed files with 546 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### PhpStorm ###
JetBrains.gitignore
+1
View File
@@ -0,0 +1 @@
Konstantinos Pappas <konpap@pressidium.com>
+1
View File
@@ -0,0 +1 @@
@over-engineer
+23
View File
@@ -0,0 +1,23 @@
FROM alpine:3.15
LABEL maintainer="Konstantinos Pappas <konpap@pressidium.com>"
COPY LICENSE /
COPY README.md /
# Install packages
# We need `openssh-client` to run `ssh-keyscan`
# We need `git`, `python3`, and `py3-pip` to run `git-restore-mtime`
RUN apk add --no-cache bash openssh-client lftp git python3 py3-pip
# Install `git-restore-mtime` from GitHub releases
RUN wget -O ./git-restore-mtime.tar.gz https://github.com/MestreLion/git-tools/archive/refs/tags/v2022.07.tar.gz \
&& tar -xzf ./git-restore-mtime.tar.gz \
&& mv ./git-tools-2022.07/git-restore-mtime /usr/local/bin/git-restore-mtime \
&& rm -rf ./git-tools-2022.07 \
&& rm -f ./git-restore-mtime.tar.gz
# Copy our entrypoint script and make it executable
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Pressidium
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+100
View File
@@ -0,0 +1,100 @@
# lftp-mirror-action
🚀 GitHub action to mirror files via SFTP.
## Table of Contents
* [Usage](#usage)
* [Inputs](#inputs)
* [Bugs & Features](#bugs--features)
* [License](#license)
## Usage
```yaml
name: Deploy via SFTP
on: push
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Deploy files via SFTP
uses: pressidium/lftp-mirror-action@v1
with:
# SFTP credentials
host: ${{ secrets.SFTP_HOST }}
port: ${{ secrets.SFTP_PORT }}
user: ${{ secrets.SFTP_USER }}
pass: ${{ secrets.SFTP_PASS }}
# lftp settings
onlyNewer: true
settings: 'sftp:auto-confirm=yes'
# Mirror command options
localDir: '.'
remoteDir: '/var/www/html/example.com/public'
reverse: true
ignoreFile: '.lftp_ignore'
options: '--verbose'
```
## Inputs
| Parameter | Description | Required | Default |
|---------------------|---------------------------------------------------------------------------|----------|--------------------|
| `host` | The hostname of the SFTP server | Yes | N/A |
| `port` | The port of the SFTP server | No | `'22'` |
| `user` | The username to use for authentication | Yes | N/A |
| `pass` | The password to use for authentication | Yes | N/A |
| `forceSSL` | Refuse to send password in clear when server does not support SSL | No | `'true'` |
| `verifyCertificate` | Verify server’s certificate to be signed by a known Certificate Authority | No | `'true'` |
| `fingerprint` | Key fingerprint of the host we want to connect to | No | `''` |
| `onlyNewer` | Only transfer files that are newer than the ones on the remote server | No | `'true'` |
| `restoreMTime` | Restore the modification time of the files (if necessary) | No | `'true'` |
| `parallel` | Number of parallel transfers | No | `'1'` |
| `settings` | Any additional lftp settings to configure | No | `''` |
| `localDir` | The local directory to copy from (assuming `reverse` is set to `true`) | No | `'.'` |
| `remoteDir` | The remote directory to copy to (assuming `reverse` is set to `true`) | No | `'/var/www/html/'` |
| `reverse` | Whether to copy from the remote to the local or the other way around | No | `'true'` |
| `ignoreFile` | The name of the file to use as the ignore list | No | `'.lftp_ignore'` |
| `options` | Any additional `mirror` command options to configure | No | `''` |
### Fingerprint
Omitting the `fingerprint` input, defaults to accepting any fingerprint
(i.e. automatically adding the host/port to the `known_hosts` file)
### Restoring modification times
:warning: Read this section if `lftp-mirror-action` uploads _all_ files every time it runs.
Git does not preserve the original modification time of committed files.
When repositories are cloned, branches are checked out, etc., the modification time
of the files is updated to the current time. This means that the modification time
of the files on the GitHub runner will always be newer than the files on the SFTP server,
which will result in the `mirror` command always uploading all files (since `lftp` determines
whether a file has changed based on its file size and timestamp).
To prevent this, if `onlyNewer` is set to `true`, we restore the modification time of the files
based on the date of the most recent commit that modified them. You can disable this behavior
by setting `restoreMTime` to `false` (useful if you've already run an action like
[`git-restore-mtime-action`](https://github.com/chetan/git-restore-mtime-action)
in your GitHub Actions workflow).
If you're using [`actions/checkout`](https://github.com/actions/checkout) ≥ `v2` you _must_ set
`fetch-depth` to `0` in order to fetch the entire Git history.
## Bugs & Features
If you've spotted any bugs, or would like to request additional features from this
GitHub Action, please [open an issue](https://github.com/pressidium/lftp-mirror-action/issues).
## License
The MIT License, check the `LICENSE` file.
+67
View File
@@ -0,0 +1,67 @@
name: 'lftp-mirror-action'
author: 'Pressidium'
description: 'Upload or update a directory tree via SFTP'
inputs:
host:
description: 'The hostname of the SFTP server'
required: true
port:
description: 'The port of the SFTP server'
required: false
default: '22'
user:
description: 'The username to use for authentication'
required: true
pass:
description: 'The password to use for authentication'
required: true
forceSSL:
description: 'Refuse to send password in clear when server does not support SSL'
required: false
default: 'true'
verifyCertificate:
description: 'Verify server’s certificate to be signed by a known Certificate Authority'
required: false
default: 'true'
fingerprint:
description: 'Key fingerprint of the host we want to connect to'
required: false
default: ''
onlyNewer:
description: 'Only transfer files that are newer than the ones on the remote server'
required: false
default: 'true'
parallel:
description: 'Number of parallel transfers'
required: false
default: '1'
settings:
description: 'Any additional lftp settings to configure'
required: false
default: ''
localDir:
description: 'The local directory to copy from'
required: false
default: '.'
remoteDir:
description: 'The remote directory to copy to'
required: false
default: '/var/www/html/'
reverse:
description: 'Whether to copy from the remote to the local directory'
required: false
default: 'true'
ignoreFile:
description: 'The name of the file to use as the ignore list'
required: false
default: '.lftp_ignore'
options:
description: 'Any additional mirror command options to configure'
required: false
default: ''
runs:
using: 'docker'
image: 'Dockerfile'
branding:
icon: 'upload-cloud'
color: 'purple'
+301
View File
@@ -0,0 +1,301 @@
#!/bin/bash
# ----------------------------------------------------------------------------
# lftp-mirror-action (GitHub Action)
# Copyright (c) 2022 Pressidium
# ----------------------------------------------------------------------------
# GitHub will automatically create an environment variable for each input
# formatted like: `INPUT_<VARIABLE_NAME>` (input names are converted to
# uppercase letters and any spaces are replaced with underscores)
#
# Available environment variables:
# ---
# - `INPUT_HOST` — The hostname of the SFTP server (required)
# - `INPUT_PORT` — The port of the SFTP server
# - `INPUT_USER` — The username to use for authentication (required)
# - `INPUT_PASS` — The password to use for authentication (required)
# - `INPUT_FORCESSL` — Refuse to send password in clear when server does not support SSL
# - `INPUT_VERIFYCERTIFICATE` — Verify server's certificate to be signed by a known Certificate Authority
# - `INPUT_FINGERPRINT` — The key fingerprint of the host we want to connect to
# - `INPUT_ONLYNEWER` — Only transfer files that are newer than the ones on the remote server
# - `INPUT_PARALLEL` — Number of parallel transfers
# - `INPUT_SETTINGS` — Any additional lftp settings to configure
# - `INPUT_LOCALDIR` — The local directory to copy to (assuming `reverse` is set to `true`)
# - `INPUT_REMOTEDIR` — The remote directory to copy to (assuming `reverse` is set to `true`)
# - `INPUT_REVERSE` — Whether to copy from the remote server to the local machine or the other way around
# - `INPUT_IGNOREFILE` — The name of the file containing the ignore list
# - `INPUT_OPTIONS` — Any additional `mirror` command options to configure
# Global variable to keep track of the `lftp` settings
settings=""
# Global variable to keep track of flags for the lftp `mirror` command
flags=""
###########################################################################
# Check whether the given path is an existing file.
# Arguments:
# The path to check.
# Returns:
# 0 if the path is an existing file, 1 otherwise.
###########################################################################
file_exists () {
if [[ -f "$1" ]]; then
return 0
else
return 1
fi
}
###########################################################################
# Trim leading and trailing whitespace from the given string.
# Arguments:
# The string to trim.
# Outputs:
# The trimmed string.
###########################################################################
trim_whitespace () {
echo "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
}
###########################################################################
# Print the lowercase version of the input variable.
# Arguments:
# The string to print in lowercase.
# Outputs:
# The lowercase version of the input string.
###########################################################################
to_lower () {
echo "$1" | tr '[:upper:]' '[:lower:]'
}
###########################################################################
# Check whether the input variable is truthy.
# Arguments:
# The string to check.
# Returns:
# 0 if the value is truthy, 1 otherwise.
###########################################################################
is_true () {
local value
value=$(to_lower "$1")
if [[ "${value}" = "true" ]] || [[ "${value}" = "yes" ]] || [[ "${value}" = "1" ]]; then
return 0
else
return 1
fi
}
###########################################################################
# Append the given flag to `${flags}`.
# Globals:
# flags
# Arguments:
# The flag to append.
###########################################################################
append_flag () {
local flag="$1"
if [[ "${flag}" =~ ^-- ]]; then
flags="${flags} ${flag}"
else
flags="${flags} --${flag}"
fi
}
###########################################################################
# Append the given flag to `${flags}` if its value is truthy.
# Arguments:
# The name of the flag to append.
# The value of the flag.
###########################################################################
eval_flag () {
local flag_name="$1"
local flag_value="$2"
if is_true "${flag_value}"; then
append_flag "${flag_name}"
fi
}
###########################################################################
# Print 'yes' or 'no' depending on whether the input variable is truthy.
# Arguments:
# The string to check.
# Outputs:
# 'yes' if the input variable is truthy, 'no' otherwise.
###########################################################################
print_yes_no_opt () {
if is_true "$1"; then
echo "yes"
else
echo "no"
fi
}
###########################################################################
# Append the given setting to `${settings}`.
# Globals:
# settings
# Arguments:
# The setting to append.
###########################################################################
handle_setting () {
local setting="$1"
local setting_name
local setting_value
setting_name=$(echo "${setting}" | cut -d '=' -f 1)
setting_value=$(print_yes_no_opt "$(echo "${setting}" | cut -d '=' -f 2)")
settings="${settings} set ${setting_name} ${setting_value};"
}
###########################################################################
# Check whether all required environment variables are set.
# Globals:
# INPUT_HOST
# INPUT_USER
# INPUT_PASS
###########################################################################
check_required_envs () {
local required_envs
required_envs=(
INPUT_HOST
INPUT_USER
INPUT_PASS
)
for env in "${required_envs[@]}"; do
if [[ -z "${!env}" ]]; then
echo "Error: ${env} is not set"
exit 1
fi
done
}
###########################################################################
# Split the given string into an array using the given delimiter.
# Arguments:
# The string to split.
# The delimiter to use.
# Outputs:
# The array.
###########################################################################
split_string () {
local string="$1"
local delimiter="$2"
local IFS="${delimiter}"
read -ra parts <<< "${string}"
echo "${parts[@]}"
}
###########################################################################
# Check whether the given string represents a valid (positive) integer.
# Arguments:
# The string to check.
# Returns:
# 0 if the string represents a valid integer (>= 0), 1 otherwise.
###########################################################################
string_represents_an_integer() {
local string="$1"
if [[ "${string}" =~ ^[0-9]+$ ]]; then
return 0
else
return 1
fi
}
###########################################################################
# Print the command we're about to run, and run it.
# Arguments:
# The command to run.
# Outputs:
# The command to run.
###########################################################################
debug_run_cmd () {
# Automatically revert any changes to shell
# options when the function returns
local -
set -o xtrace
"$@"
}
# Check whether all required environment variables are set
check_required_envs
# Set a variable for files to exclude
excluded_files=""
if [[ -n "${INPUT_IGNOREFILE}" ]] && file_exists "${INPUT_IGNOREFILE}"; then
excluded_files=$(< "${INPUT_IGNOREFILE}" grep -v '^#' | grep -v '^$' | sed 's/^/-X /' | tr '\n' ' ')
fi
if [[ -n "${INPUT_FINGERPRINT}" ]]; then
# If a fingerprint is set, we add it to the `known_hosts` file
echo "Adding fingerprint to the known_hosts file"
echo "${INPUT_HOST} ${INPUT_FINGERPRINT}" >> /root/.ssh/known_hosts
else
# If a fingerprint is not set, we automatically add the host to the `known_hosts` file
echo "Adding host to the known_hosts file"
mkdir -p /root/.ssh \
&& chmod 0700 /root/.ssh \
&& ssh-keyscan -H -p "${INPUT_PORT}" "${INPUT_HOST}" >> /root/.ssh/known_hosts
fi
# Iterate over settings
if [[ -n "${INPUT_SETTINGS}" ]]; then
for setting in $(split_string "${INPUT_SETTINGS}" ","); do
handle_setting "$(trim_whitespace "${setting}")"
done
fi
# Handle settings with explicit parameters
handle_setting "ftp:ssl-force=${INPUT_FORCESSL}"
handle_setting "ssl:verify-certificate=${INPUT_VERIFYCERTIFICATE}"
# Trim leading and trailing whitespace from the settings
settings=$(trim_whitespace "${settings}")
# Iterate over options and append any flags for the ltp `mirror` command
if [[ -n "${INPUT_OPTIONS}" ]]; then
for option in ${INPUT_OPTIONS}; do
append_flag "${option}"
done
fi
# Evaluate flags for the mirror command
eval_flag "reverse" "${INPUT_REVERSE}"
eval_flag "only-newer" "${INPUT_ONLYNEWER}"
# Evaluate the parallel input
if string_represents_an_integer "${INPUT_PARALLEL}" && [[ "${INPUT_PARALLEL}" -gt 1 ]]; then
append_flag "parallel=${INPUT_PARALLEL}"
else
echo "Ignoring invalid value for the 'parallel' input: ${INPUT_PARALLEL}"
fi
# Restore timestamps if the `--only-newer` flag is set
if is_true "${INPUT_ONLYNEWER}"; then
echo "Restoring the original modification time of files based on the date of the most recent commit..."
# Disable Git ownership check
git config --global --add safe.directory '*'
# Run `git-restore-mtime`
/usr/bin/python3 /usr/local/bin/git-restore-mtime --verbose
fi
# Transfer files via SFTP
debug_run_cmd lftp -u "${INPUT_USER},${INPUT_PASS}" -p "${INPUT_PORT}" "sftp://${INPUT_HOST}" \
-e "${settings} \
mirror ${excluded_files} \
${flags} ${INPUT_LOCALDIR} ${INPUT_REMOTEDIR}; \
bye"