mirror of
https://github.com/pressidium/lftp-mirror-action.git
synced 2024-08-31 16:10:06 +00:00
Initial commit
This commit is contained in:
+32
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
@over-engineer
|
||||
+23
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
@@ -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'
|
||||
Executable
+301
@@ -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"
|
||||
Reference in New Issue
Block a user