Rebuilt repo, wrote README, cleaned out script

main
capntack 2 years ago
parent 5332c77ce8
commit ba5ea3c63f

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <https://unlicense.org>

@ -0,0 +1,179 @@
# Rsync and Restic Backup Scripts
> Thank you for visiting! If you are viewing this repo on GitHub or GitLab, please note that it is just a mirror. Please visit the [originating repo](https://tacksupport.net/git/capntack/FOSview) for any comments, issues, pull requests, etc. You can sign in with your GitHub or GitLab account via Oauth2.
<br>
> **Disclaimer:** As with anything to do with your data, you should read and understand what this script does before applying it. I am not responsible for any mishaps. Even if only replace the variabls and run it as I do, me saying "it works on my machine" should not be sufficient.
<br>
A script to perform incremental backups using [rsync](https://github.com/WayneD/rsync) and [restic](https://github.com/restic/restic). Why both? Isn't that redundant? Well, I wanted to have some of my backups to be more quickly retrievable. Like an on prem "cloud" storage. Rsync fits the bill for that. But I also wanted to backup and compress larger swaths of data for longer term storage. And that's where Restic comes in. Eventually I will rewrite so that rsync runs more often than restic to keep directories and files that are modified more often as up to date as possible.
This script assumes you are running Linux, and have at least basic working knowledge of it and bash scripting. It "works on my machine" which is currently running Pop!_OS 22.04 LTS.
<br>
### Installation, Prep, and Configuration
1. Install rsync, restic, and depencies for the script:
```bash
apt install rsync restic moreutils # moreutils installs the `ts` command for timestamping the logs
```
<br>
2. Create the directory where rsync will backup to:
```bash
mkdir /path/to/dir/to/backup/to
```
<br>
3. Copy the rsync manifest template to the script's root directory, rename it as you like, and then fill it out. This will allow the `--include-from` option to only backup what you want. There is some comments in the template, but the gist of it is that the file is read in order. The initial include, `+ */` includes the RSYNC_SOURCE variable from the script all directories within, recursively. The following lines are where you specify the directories and files you explicitely want to backup. The final line, `- *` excludes everything that wasn't explicitely included prior. This allows you to choose a higher directory, say $HOME, but pick and choose what you want within it instead of rsyncing the whole thing. The script also includes the `--prune-empty-dirs` option, which will prevent it from syncing all the empty directory folders within the directoris along the path to what you actually want at the end of it.
<br>
4. Ensure restic is up to date, in case the version from your repos are behind:
```bash
restic self-update
```
<br>
5. Initialize the restic "repo" (what restic calls the backup destination):
```bash
restic init --repo /path/to/repo
```
<br>
Create your repo password when prompted. Do not lose this, as you will otherwise be unable to access your backups. I would suggest a password manager. And possibly a physical copy stored in a very safe place.
<br>
6. Verify your repo is at least version 2, in order to support compression:
```bash
restic -p $REPO_PASSWORD -r path/to/repo cat config
```
If it isn't, you may need to revisit step 4 and figure out why your install isn't up to date. Then recreate the repo (you can just delete the faulty repo directory to get rid of it).
<br>
7. Run the first restic backup. This will take a while, depending on how much data you have. 250 GB took me about an hour and a half. Edit, remove, or add to the tags as desired. Tags can be shared between repos in various combinations. They can be used to search for, query, and prune your backups from their various sources. The `--exclude-caches` option will exclude directories containing the `CACHEDIR.TAG` file. Which isn't all caches, but it's a happy medium between not excluding any, and having to script/search them all out. Pay attention to lack of trailing slashes.
> Note: if you ever run this command as sudo, whether in your terminal or as a cronjob or any other way, you must always run it and other commands against that repo as sudo. So make your choice now.
```bash
restic backup --verbose --compression max \
-p $REPO_PASSWORD \
-r /path/to/repo \
--tag $TAG1 --tag $TAG2 \
--exclude-caches \
/path/to/source
```
<br>
8. Verify your backup by first fetching the snapshot ID:
```bash
restic -p $REPO_PASSWORD -r /path/to/repo snapshots
```
Then list the files within to verify eveything is there:
```bash
restic ls -p $REPO_PASSWORD -r /path/to/repo --long $SNAPSHOT_ID
```
Then compare the backup size to the size of the source (this will retrieve the uncompressed size of the repo):
```bash
restic ls -p $REPO_PASSWORD -r /path/to/repo stats $SNAPSHOT_ID
```
And finally, check the integrity of the repo:
```bash
restic -p $REPO_PASSWORD -r /path/to/repo check
```
<br>
9. Copy the restic password template to the script's root directoy, rename it as you like, and replace all text within it with just the password. Then secure the file:
```bash
sudo chmod 600 /path/to/restic/password/.file
```
<br>
10. Copy the script template to the script's root directory, rename as you like, and then fill out the variables the comments call out. Pay attention to where leading/trailing slashes are omitted. That is on purpose. I find it's best to use absolute paths, that way if you every move the script to a different directory, it won't break. A few notes and definitions above and beyond the comments in the script:
a, The script dumps a log of its output into a directory of your choosing (the first variable in the script). There's a directory in script's root directory for that, but feel free to put them wherever you like.
b. The script includes variables and script for both a second rsync and a second restic source/destination. You can add more or remove them as you like. Just note that each rsync really should have a separate source, destination, and manifest. While restic can have multiple sources syncing to the same repo, which also increases the benefit from its deduplication. You can also mix and match tags (tho I would advise against using the exact same set of tags on two different sources). Tho, while you can use the same password for each source, maybe don't?
c. By default, rsync will backup incrementally, but not track version history. The script gets around this by putting each new backup into its own dated directory, and then hardlinking to the inodes of already backed up files, and only backing up new files. The `--delete` option in this case simply doesn't backup a file instead of deleting it at the destination. A "latest" folder is also created for both the script to check against and for ease of finding the lastest backup. This leads us to...
d. The rsync script also allows for days of retention. After which older backup directories are deleted. And, thanks to hardlinking, files that were initially backed up in it are not deleted if they are hardlinked in any subsequent backup. `$RSYNC_RETENTION_DAYS` variables are calculated thusly: # of days wanted (i.e. 7) + the latest directory (1) + 1. So in this case, to keep 7 days worth of versioning, you would use a 9 for this variable.
e. The rsync includes a hacky fix for an issue I ran into rsyncing to an NFS destination. After backing up to the new directy as desired and updating the `latest` hardlink, the timestamps of both would change to the most recent date for the timestamp of 21:20. I have no idea why. And that would mess with the retention if I ran the backup multiple times in a day. As they would all have the same timestamp. So in between updating the `latest` hardlink and running the retention policy, the script runs a `touch` on a `timestamp.fix` file within the `$RSYNC_DEST_PATH`, which fixes the timestamps. If you aren't backing up to an NFS destination, you likely don't need this. And if you know why this is happening, please let me know. Or clone the repo, patch it, and do a pull request so that your fix can be tested and included.
f. Pay attention to the restic tags in the script. When the script runs the forget and prune commands, it will run that against the entire repo. So you want to ensure the tags in that command match the backups you want it to actually affect. I would suggest, after running the initial backup in step 7 and then have the script ready, run it and then run the verification steps from step 8 again. Just to be sure you have it right. And if you have multiple sources going to the same repo, do the same. You can also perform [dry runs](https://restic.readthedocs.io/en/latest/060_forget.html#removing-snapshots-according-to-a-policy) on removal polices (and [on backups](https://restic.readthedocs.io/en/latest/040_backup.html#dry-runs) too, btw) to sanity check yourself before accidentally nuking your repo. See the disclaimer at the start of this README.
g. Regarding the compression level of the restic backup, you can choose `off`, `auto`, or `max`. I ran a super scientific one run each on my backup source and got the following results:
Raw Data: 255 GB to Backup <br>
All levels of compression also deduplicate files <br>
Compression Off: 249.5 GB = 97.8% compression <br>
Compression Auto: 208.4 GB = 81.7% compression <br>
Compression Max: 206.1 GB = 80.8% compression
I did not note the time it took, but I want to say it was about an hour when set to `off`, and about an hour and a half for both `auto` and `max`. I personally use the `max` setting, as it didn't take much longer. And while it didn't really compress that much more, I feel like as I add more sources to the repo, it will slowly add up to better gains. But you can decide as you like for your own backups.
<br>
11. Make the script executable:
```bash
sudo chmod u+x /path/to/script.sh
```
<br>
12. At this point, you can decide how you will run this script going forward. Whether just as you remember it (not very reliable), or set reminders for it (more reliable), or automate it in some way (best) like a crontab:
```bash
crontab -e
```
Paste something like the following to the end of your crontab:
```bash
0 0 * * * cd /path/to/script/dir/ && ./script.sh
```
You can avoid having to have crontab cd into the script's directory if you place it somewhere in your path. If you do, I would suggest copying the script you just edited to said path folder. That way you can fiddle with and test it without messing with your production script. Then replace the prod script once you have any tweaks figured out.
<br>
### Sources, Inspiration, and Further Reading
- [Rsync's Documentation](https://rsync.samba.org/documentation.html)
- [Restic's Documentation](https://restic.readthedocs.io/en/latest/010_introduction.html)
- [Inspiration for the base rsync script](https://linuxconfig.org/how-to-create-incremental-backups-using-rsync-on-linux)
- [Inspiration for how to format the include-from-file](https://stackoverflow.com/a/32527277)
- [Inspiration for command in the rsync script to delete all but the most recent directories](https://stackoverflow.com/a/4127056)
- [Inspiration for the base restic script](https://codeberg.org/Taffer/restic-scripts)
- [More Inspiration for the base restic script](https://forum.yunohost.org/t/daily-automated-backups-using-restic/16812)
- [Inspiration for code to log to the console and log file](https://unix.stackexchange.com/a/574542)
- [Inspiration for adding timestamps to the logfile](https://stackoverflow.com/a/39239416)
- [Inspiration for adding a timestamp to end of script command](https://www.baeldung.com/linux/prepend-timestamp-command-output)

@ -0,0 +1,8 @@
# Roadmap
- Remove need for the timestamp.fix command
- Add in SSH capability the rsync portion of the script
- Add in include/exclude files for restic
- Add in capability for rsync to run more often than restic, all from the same script
- Move variables to an external file
- Script/code out all caches

@ -0,0 +1 @@
Placeholder so `backup-logs` pushes to repo. Feel free to delete.

@ -1,23 +1,12 @@
#!/bin/bash #!/bin/bash
####################### #############
# backups-template.sh # # VARIABLES #
####################### #############
### A script to perform incremental backups using rsync and restic # Script-wide variables and commands. You only need to configure the first one.
### Why both? Rsync for quickly browsable and retrievable backups readonly LOG_DIR="/path/to/log/dir"
### Restic for larger and longer term backups
# Throughout, take note of leading/trailing forward slashes
# Be sure to make script executable:
# chmod u+x ./script.sh
# If you add this script to your crontab, either use all absolute paths...
# Or add add a cd command into the script's dir...
# i.e.: 0 0 * * * cd /path/to/script/dir/ && ./script.sh
# If you run this as root, future runs must also be as root
# Script-wide variables and commands
readonly DATETIME="$(date '+%Y-%m-%d_%H:%M:%S')" readonly DATETIME="$(date '+%Y-%m-%d_%H:%M:%S')"
readonly LOG_DIR="/path/to/log/dir" # Configure this variable
readonly BACKUP_LOG="${LOG_DIR}/backup-log_"${DATETIME}".log" readonly BACKUP_LOG="${LOG_DIR}/backup-log_"${DATETIME}".log"
set -o errexit set -o errexit
@ -25,56 +14,53 @@ set -o nounset
set -o pipefail set -o pipefail
exec 3<&1 4<&2 exec 3<&1 4<&2
trap "exec 2<&4 1<&3" 0 1 2 3 trap "exec 2<&4 1<&3" 0 1 2 3
# Needs moreutils installed for logging timestamps to work. Otherwise, comment next line and uncomment the one after # If moreutils is not installed, comment out or delete the next line and uncomment the one after
exec > >(tee >(ts "%Y-%m-%d_%H:%M:%S" > "${BACKUP_LOG}")) 2>&1 exec > >(tee >(ts "%Y-%m-%d_%H:%M:%S" > "${BACKUP_LOG}")) 2>&1
# exec > >(tee "${BACKUP_LOG}") 2>&1 # exec > >(tee "${BACKUP_LOG}") 2>&1
# Rsync Manifest 01 Variables # Rsync Manifest 01 Variables
readonly RSYNC_SOURCE_01="/path/to/dir/to/backup/01" # Configure this variable # Configure variables from here...
readonly RSYNC_DEST_01="/path/to/dir/to/backup/to/01" # Configure this variable readonly RSYNC_SOURCE_01="/path/to/dir/to/backup/01"
readonly RSYNC_DEST_01="/path/to/dir/to/backup/to/01"
readonly RSYNC_MANIFEST_01="/path/to/manfiest/01/file.conf"
readonly RSYNC_RETENTION_DAYS_01="9"
# ...to here
readonly RSYNC_DEST_PATH_01="${RSYNC_DEST_01}/${DATETIME}" readonly RSYNC_DEST_PATH_01="${RSYNC_DEST_01}/${DATETIME}"
readonly RSYNC_LATEST_LINK_01="${RSYNC_DEST_01}/latest" readonly RSYNC_LATEST_LINK_01="${RSYNC_DEST_01}/latest"
readonly RSYNC_MANIFEST_01="/path/to/manfiest/01/file.conf" # Configure this variable
# Math is number of versions wanted + 1 for latest hardlink + 1 # Rsync Manifest 02 Variables
# So 9 would retain 7 days # Configure variables from here...
readonly RSYNC_RETENTION_DAYS_01="9" # Configure this variable readonly RSYNC_SOURCE_02="/path/to/dir/to/backup/02"
readonly RSYNC_DEST_02="/path/to/dir/to/backup/to/02"
# Rsync Manifest 02 Variables. Remove if only using one manifest. Duplicate and increment if using more. readonly RSYNC_MANIFEST_02="/path/to/manfiest/02/file.conf"
readonly RSYNC_SOURCE_02="/path/to/dir/to/backup/02" # Configure this variable readonly RSYNC_RETENTION_DAYS_02="9"
readonly RSYNC_DEST_02="/path/to/dir/to/backup/to/02" # Configure this variable # ...to here
readonly RSYNC_DEST_PATH_02="${RSYNC_DEST_02}/${DATETIME}" readonly RSYNC_DEST_PATH_02="${RSYNC_DEST_02}/${DATETIME}"
readonly RSYNC_LATEST_LINK_02="${RSYNC_DEST_02}/latest" readonly RSYNC_LATEST_LINK_02="${RSYNC_DEST_02}/latest"
readonly RSYNC_MANIFEST_02="/path/to/manfiest/02/file.conf" # Configure this variable
readonly RSYNC_RETENTION_DAYS_02="9" # Configure this variable
# Restic Backup 01 Variables. # Restic Backup 01 Variables. Configure all accordingly.
readonly RESTIC_PASSWORD_01="/path/to/restic/password/01.file" # Configure this variable readonly RESTIC_PASSWORD_01="/path/to/restic/password/01.file"
readonly RESTIC_SOURCE_01="/path/to/restic/repo-01" # Configure this variable readonly RESTIC_SOURCE_01="/path/to/restic/repo-01"
readonly RESTIC_REPO_01="/path/to/backup/dest-01" # Configure this variable readonly RESTIC_REPO_01="/path/to/backup/dest-01"
readonly RESTIC_COMPRESSION_01="max" readonly RESTIC_COMPRESSION_01="max"
# RE: Retention policy commands, please pay attention in the code, as you can restrict by tags readonly RESTIC_RETENTION_DAYS_01="7"
# Otherwise this script will apply the retention policy to ALL snapshots from all sources in the repo readonly RESTIC_RETENTION_WEEKS_01="4"
readonly RESTIC_RETENTION_DAYS_01="7" # Configure this variable readonly RESTIC_RETENTION_MONTHS_01="6"
readonly RESTIC_RETENTION_WEEKS_01="4" # Configure this variable readonly RESTIC_RETENTION_YEARS_01="1"
readonly RESTIC_RETENTION_MONTHS_01="6" # Configure this variable readonly RESTIC_TAG_01="tag01"
readonly RESTIC_RETENTION_YEARS_01="1" # Configure this variable readonly RESTIC_TAG_02="tag02"
# Tags can be used on backups from multiple sources, and more than one tag can be used on one source
# Mix and combine as desired # Restic Backup 02 Variables. Configure all accordingly.
readonly RESTIC_TAG_01="tag01" # Configure this variable readonly RESTIC_PASSWORD_02="/path/to/restic/password/02.file"
readonly RESTIC_TAG_02="tag02" # Configure this variable readonly RESTIC_SOURCE_02="/path/to/restic/repo-02"
readonly RESTIC_REPO_02="/path/to/backup/dest-02"
# Restic Backup 02 Variables. Note that you can use the same repo and password for multiple sources.
# Or separate ones. Configure to your own needs.
readonly RESTIC_PASSWORD_02="/path/to/restic/password/02.file" # Configure this variable
readonly RESTIC_SOURCE_02="/path/to/restic/repo-02" # Configure this variable
readonly RESTIC_REPO_02="/path/to/backup/dest-02" # Configure this variable
readonly RESTIC_COMPRESSION_02="max" readonly RESTIC_COMPRESSION_02="max"
readonly RESTIC_RETENTION_DAYS_02="7" # Configure this variable readonly RESTIC_RETENTION_DAYS_02="7"
readonly RESTIC_RETENTION_WEEKS_02="4" # Configure this variable readonly RESTIC_RETENTION_WEEKS_02="4"
readonly RESTIC_RETENTION_MONTHS_02="6" # Configure this variable readonly RESTIC_RETENTION_MONTHS_02="6"
readonly RESTIC_RETENTION_YEARS_02="1" # Configure this variable readonly RESTIC_RETENTION_YEARS_02="1"
readonly RESTIC_TAG_03="tag03" # Configure this variable readonly RESTIC_TAG_03="tag03"
readonly RESTIC_TAG_04="tag04" # Configure this variable readonly RESTIC_TAG_04="tag04"
################### ###################
# RSYNC SCRIPT(S) # # RSYNC SCRIPT(S) #
@ -84,9 +70,6 @@ readonly RESTIC_TAG_04="tag04" # Configure this variable
mkdir -p "${RSYNC_DEST_01}" mkdir -p "${RSYNC_DEST_01}"
# -avP will tell rsync to run in archive mode, be verbose, keep partial files if interrupted, and show progress # -avP will tell rsync to run in archive mode, be verbose, keep partial files if interrupted, and show progress
# --delete will delete any folders in latest that are no longer present in source
# --prune-empty-dirs will not sync any empty dirs, essential to work correctly with the manifests
# --include-from pulls what dirs/paths/files to sync from the manifest
rsync -avP --delete --prune-empty-dirs --include-from="${RSYNC_MANIFEST_01}" \ rsync -avP --delete --prune-empty-dirs --include-from="${RSYNC_MANIFEST_01}" \
"${RSYNC_SOURCE_01}/" \ "${RSYNC_SOURCE_01}/" \
--link-dest "${RSYNC_LATEST_LINK_01}" \ --link-dest "${RSYNC_LATEST_LINK_01}" \
@ -96,9 +79,7 @@ rsync -avP --delete --prune-empty-dirs --include-from="${RSYNC_MANIFEST_01}" \
rm -rf "${RSYNC_LATEST_LINK_01}" rm -rf "${RSYNC_LATEST_LINK_01}"
ln -s "${RSYNC_DEST_PATH_01}" "${RSYNC_LATEST_LINK_01}" ln -s "${RSYNC_DEST_PATH_01}" "${RSYNC_LATEST_LINK_01}"
# A hacky fix for an issue where the -a switch is required for rsync to compare time stamps for incremental backups # The hacky fix for the NFS destination timestamp bug
# But the ${BACKUP_PATH} dir's time stamp gets messed up when doing this over NFS
# (will be patched out once a better fix is in place)
touch "${RSYNC_DEST_PATH_01}"/timestamp.fix touch "${RSYNC_DEST_PATH_01}"/timestamp.fix
# This will prune excess version folders. # This will prune excess version folders.
@ -108,10 +89,8 @@ rm -rf `ls -t | tail -n +"${RSYNC_RETENTION_DAYS_01}"`
# CD backup to script dir to reset for next steps # CD backup to script dir to reset for next steps
cd - cd -
# If you are only using one manifest, feel free to delete the next chunk # Remove the rest of the rsync script if you are only running on source -> destination
# Copy and paste it as many times as you need manifests, ensuring to increment variables # Or, copy, paste, and configure as many times as needed. Remember to also update variables up above
# First we cd back to the script's dir, then continue as normal (can remove if using absolute paths for all variables)
mkdir -p "${RSYNC_DEST_02}" mkdir -p "${RSYNC_DEST_02}"
rsync -avP --delete --prune-empty-dirs --include-from="${RSYNC_MANIFEST_02}" \ rsync -avP --delete --prune-empty-dirs --include-from="${RSYNC_MANIFEST_02}" \
@ -130,10 +109,7 @@ cd -
# RESTIC SCRIPT(S) # # RESTIC SCRIPT(S) #
#################### ####################
# --p points to the password file # --p points to the password file, -r points to the restic repo path
# -r points to the restic repo path
# --tag will tag the snapshot, repeat as many times as necessary
# The final line outputs a log file
restic backup --verbose --compression "${RESTIC_COMPRESSION_01}" \ restic backup --verbose --compression "${RESTIC_COMPRESSION_01}" \
-p "${RESTIC_PASSWORD_01}" \ -p "${RESTIC_PASSWORD_01}" \
-r "${RESTIC_REPO_01}" \ -r "${RESTIC_REPO_01}" \
@ -155,8 +131,7 @@ restic check \
-p "${RESTIC_PASSWORD_01}" \ -p "${RESTIC_PASSWORD_01}" \
-r "${RESTIC_REPO_01}" -r "${RESTIC_REPO_01}"
# If you are only backing up from one source, feel free to delete the next chunk # Remove or copy and paste as needed. Remember: multiple sources and tags can go to the same repo.
# Copy and paste it as many times as you have sources, ensuring to increment variables
restic backup --verbose --compression "${RESTIC_COMPRESSION_02}" \ restic backup --verbose --compression "${RESTIC_COMPRESSION_02}" \
-p "${RESTIC_PASSWORD_02}" \ -p "${RESTIC_PASSWORD_02}" \
-r "${RESTIC_REPO_02}" \ -r "${RESTIC_REPO_02}" \
@ -182,16 +157,3 @@ restic check \
# End of script message in log # End of script message in log
echo > >(tee >(echo "$(ts "%Y-%m-%d_%H:%M:%S") Backup Script Complete" >> "${BACKUP_LOG}")) echo > >(tee >(echo "$(ts "%Y-%m-%d_%H:%M:%S") Backup Script Complete" >> "${BACKUP_LOG}"))
#############
# FOOTNOTES #
#############
# Inspiration for the base rsync script: https://linuxconfig.org/how-to-create-incremental-backups-using-rsync-on-linux
# Inspiration for how to format the include-from-file: https://stackoverflow.com/a/32527277
# Inspiration for command in the rsync script to delete all but the most recent directories: https://stackoverflow.com/a/4127056
# Inspiration for the base restic script: https://codeberg.org/Taffer/restic-scripts
# More Inspiration for the base restic script: https://forum.yunohost.org/t/daily-automated-backups-using-restic/16812
# Inspiration for the logging to console and file function: https://unix.stackexchange.com/a/574542
# Inspiration for adding timestamps to the logfile: https://stackoverflow.com/a/39239416
# Inspiration for adding timestamp to end of script line: https://www.baeldung.com/linux/prepend-timestamp-command-output
Loading…
Cancel
Save