Skip to content

Commit

Permalink
Merge pull request #357 from outdoorbits/migrate_to_proftp
Browse files Browse the repository at this point in the history
allow ftp as backup source to most targets
  • Loading branch information
outdoorbits authored Feb 18, 2025
2 parents fa09219 + ac570c9 commit 313a38b
Show file tree
Hide file tree
Showing 16 changed files with 269 additions and 50 deletions.
1 change: 1 addition & 0 deletions etc/proftpd_lbb_DefaultRoot.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DefaultRoot /media lbb
36 changes: 36 additions & 0 deletions etc/proftpd_proftpd.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
LoadModule mod_tls.c

SyslogLevel debug
TransferLog /var/log/proftpd/proftpd.log

<Limit LOGIN>
AllowUser pi
AllowUser lbb
DenyAll
</Limit>

<Global>
DefaultRoot ~/ !lbb
Include /etc/proftpd/lbb_DefaultRoot.conf
</Global>

# Enable FTPS (Explicit TLS)
<IfModule mod_tls.c>
TLSEngine on
TLSRequired on
TLSProtocol TLSv1.2 TLSv1.3
TLSRSACertificateFile /etc/ssl/certs/little-backup-box.crt
TLSRSACertificateKeyFile /etc/ssl/private/little-backup-box.key
TLSOptions NoSessionReuseRequired
TLSVerifyClient off
</IfModule>

# Disable SFTP in ProFTPD (handled by SSH)
<IfModule mod_sftp.c>
SFTPEngine off
</IfModule>

# Allow only TLS-protected logins (except local network)
<IfModule mod_auth.c>
RequireValidShell off
</IfModule>
18 changes: 0 additions & 18 deletions etc/vsftpd.conf

This file was deleted.

19 changes: 8 additions & 11 deletions install-little-backup-box.sh
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,13 @@ sudo DEBIAN_FRONTEND=noninteractive \
-o "Dpkg::Options::=--force-confold" \
-o "Dpkg::Options::=--force-confdef" \
install -y -q --allow-downgrades --allow-remove-essential --allow-change-held-packages \
acl git screen rsync exfat-fuse exfatprogs ntfs-3g acl bindfs gphoto2 libimage-exiftool-perl php php-cli samba samba-common-bin vsftpd imagemagick curl libimobiledevice6 ifuse sshpass f3 sqlite3 php-sqlite3 ffmpeg libheif-examples libraw-bin openvpn wireguard openresolv hfsprogs fuse3 python3 python3-pip python3-pil python3-configobj python3-gpiozero python3-rpi-lgpio python3-qrcode python3-psutil smartmontools dos2unix
acl git screen rsync exfat-fuse exfatprogs ntfs-3g acl bindfs gphoto2 libimage-exiftool-perl php php-cli samba samba-common-bin proftpd-basic proftpd-mod-crypto imagemagick curl libimobiledevice6 ifuse sshpass f3 sqlite3 php-sqlite3 ffmpeg libheif-examples libraw-bin openvpn wireguard openresolv hfsprogs fuse3 python3 python3-pip python3-pil python3-configobj python3-gpiozero python3-rpi-lgpio python3-qrcode python3-psutil smartmontools dos2unix

# Remove packages not needed anymore
if [ "${SCRIPT_MODE}" = "update" ]; then
echo "apt-get purge..."
sudo DEBIAN_FRONTEND=noninteractive \
apt-get purge minidlna -y
apt-get purge minidlna vsftpd -y
fi

# Remove obsolete packages
Expand Down Expand Up @@ -557,16 +557,13 @@ sudo a2ensite little-backup-box
yes | sudo cp -f "${INSTALLER_DIR}/etc/samba_smb.conf" "/etc/samba/smb.conf"
sudo chmod 0440 "/etc/samba/smb.conf"

# Configure vsftpd
yes | sudo cp -f "${INSTALLER_DIR}/etc/vsftpd.conf" "/etc/vsftpd.conf"
sudo chmod 0440 "/etc/vsftpd.conf.conf"
# Configure proftpd
yes | sudo cp -f "${INSTALLER_DIR}/etc/proftpd_proftpd.conf" "/etc/proftpd/proftpd.conf"
yes | sudo cp -f "${INSTALLER_DIR}/etc/proftpd_lbb_DefaultRoot.conf" "/etc/proftpd/lbb_DefaultRoot.conf"
sudo chmod 644 "/etc/proftpd/proftpd.conf"
sudo chmod 644 "/etc/proftpd/lbb_DefaultRoot.conf"

sudo useradd -s /bin/false -r ftpsecure

sudo mkdir -p /var/run/vsftpd/empty
sudo chown ftpsecure /var/run/vsftpd/empty

sudo service vsftpd restart
sudo service proftpd restart

# setup graphical environment
source "${INSTALLER_DIR}/setup-graphical-environment.sh"
Expand Down
35 changes: 30 additions & 5 deletions scripts/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class backup(object):

def __init__(self, SourceName, TargetName, move_files='setup', DoRenameFiles='setup', ForceSyncDatabase=False, DoGenerateThumbnails='setup', shiftGenerateThumbnails=False, DoUpdateEXIF='setup', DeviceIdentifierPresetSource=None, DeviceIdentifierPresetTarget=None, PowerOff='setup', SecondaryBackupFollows=False):

# SourceName: one of ['anyusb', 'usb', 'internal', 'nvme', 'camera', 'cloud:SERVICE_NAME', 'cloud_rsync'] or functions: ['thumbnails', 'database', 'exif', 'rename]
# SourceName: one of ['anyusb', 'usb', 'internal', 'nvme', 'camera', 'cloud:SERVICE_NAME', 'cloud_rsync', 'ftp'] or functions: ['thumbnails', 'database', 'exif', 'rename]
# TargetName: one of ['anyusb', 'usb', 'internal', 'nvme', 'cloud:SERVICE_NAME', 'cloud_rsync']
# DoRenameFiles, DoGenerateThumbnails, DoUpdateEXIF: one of ['setup', True, False]
# ForceSyncDatabase: one of [True, False]
Expand Down Expand Up @@ -149,6 +149,8 @@ def __init__(self, SourceName, TargetName, move_files='setup', DoRenameFiles='se
except:
pass

self.TransferMode = None if self.SourceStorageType == 'ftp' else self.TransferMode

# Unmount devices, clean before backup
lib_storage.umount(self.__setup,'all')

Expand Down Expand Up @@ -228,6 +230,10 @@ def run(self):
self.finish()

def get_syncCommand(self, TransferMode, SubPathAtSource, dry_run=False):

if TransferMode is None:
return([])

syncCommand = []

## excludes
Expand Down Expand Up @@ -300,6 +306,10 @@ def get_syncCommand(self, TransferMode, SubPathAtSource, dry_run=False):
return(syncCommand)

def calculate_files_to_sync(self, singleSubPathsAtSource=None):

if self.TransferMode is None:
return(0, True)

if singleSubPathsAtSource:
checkPathsList = [singleSubPathsAtSource]
else:
Expand Down Expand Up @@ -435,6 +445,10 @@ def backup(self):
SourceStorageName = 'usb' if self.SourceName == 'anyusb' else self.SourceName
SourceStorageType = SourceStorageName

if self.SourceName =='ftp':
SourceStorageName = 'ftp'
SourceStorageType = SourceStorageName

if todoSources:
Identifier = todoSources[0]
elif Identifier_OLD:
Expand All @@ -448,18 +462,29 @@ def backup(self):
# Set the PWR LED to blink long to indicate waiting for the source device
lib_system.rpi_leds(trigger='timer',delay_on=750,delay_off=250)

if SourceStorageType in ['usb', 'internal','nvme', 'camera', 'cloud', 'cloud_rsync']:
self.SourceDevice = lib_storage.storage(StorageName=SourceStorageName, Role=lib_storage.role_Source, WaitForDevice=True, DeviceIdentifierPresetThis=Identifier, DeviceIdentifierPresetOther=self.TargetDevice.DeviceIdentifier)
if SourceStorageType in ['usb', 'internal','nvme', 'camera', 'cloud', 'cloud_rsync', 'ftp']:
self.SourceDevice = lib_storage.storage(StorageName=SourceStorageName, Role=lib_storage.role_Source, WaitForDevice=True, DeviceIdentifierPresetThis=Identifier, DeviceIdentifierPresetOther=self.TargetDevice.DeviceIdentifier, PartnerDevice=self.TargetDevice)

self.__display.message([f":{self.__lan.l('box_backup_mounting_target')}", f":{self.__lan.l(f'box_backup_mode_{self.TargetDevice.StorageType}')} {self.TargetDevice.CloudServiceName}"])
self.__display.message([f":{self.__lan.l('box_backup_mounting_source')}", f":{self.__lan.l(f'box_backup_mode_{self.SourceDevice.StorageType}')} {self.SourceDevice.CloudServiceName}"])
self.SourceDevice.mount()


elif SourceStorageType in ['thumbnails', 'database', 'exif']:
pass
else:
self.__display.message([f":{self.__lan.l('box_backup_invalid_mode_combination_1')}", f":{self.__lan.l('box_backup_invalid_mode_combination_2')}", f":{self.__lan.l('box_backup_invalid_mode_combination_3')}"])
return()

# for ftp source, the job is done now.
if SourceStorageType == 'ftp':
while self.TargetDevice.mounted():
time.sleep(5)

self.SourceDevice.umount()
self.__display.message([f":{self.__lan.l('box_backup_break1')}", f":{self.__lan.l('box_backup_break2')}"])

sys.exit()

# remember SourceStorageName for next run
if SourceStorageName=='camera':
Identifier_OLD = lib_storage.format_CameraIdentifier(self.SourceDevice.DeviceIdentifier, self.SourceDevice.CameraPort)
Expand Down Expand Up @@ -1400,7 +1425,7 @@ def __cleanup(self):
epilog = 'This script can ideally be configured and started via the Little Backup Box web UI.'
)

SourceChoices = ['anyusb', 'usb', 'internal', 'nvme', 'camera'] + CloudServices + ['cloud_rsync', 'thumbnails', 'database', 'exif', 'rename']
SourceChoices = ['anyusb', 'usb', 'internal', 'nvme', 'camera'] + CloudServices + ['cloud_rsync', 'ftp', 'thumbnails', 'database', 'exif', 'rename']
parser.add_argument(
'--SourceName',
'-s',
Expand Down
1 change: 1 addition & 0 deletions scripts/constants.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const_MOUNTPOINT_CLOUD_SOURCE="cloud_source"
const_MOUNT_LOCAL_TIMEOUT=30
const_MOUNT_CLOUD_TIMEOUT=30
const_INTERNAL_BACKUP_DIR="internal"
const_LBB_FTP_BACKUP_SUB_DIR="LBB-FTP"
const_BACKGROUND_IMAGES_DIR="backgroundimages"
const_WEB_ROOT_LBB="/var/www/little-backup-box"
const_LOGFILE="/var/www/little-backup-box/tmp/little-backup-box.log"
Expand Down
14 changes: 7 additions & 7 deletions scripts/cron_idletime.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ class idletime(object):
def __init__(self):
#definitions
self.WORKING_DIR = os.path.dirname(__file__)
self.ApacheAccessLogfile = '/var/log/apache2/lbb-access.log'
self.ApacheRcloneAccessLogfile = '/var/log/apache2/rclone-access.log'
self.vsftpdAccessLogfile = '/var/log/vsftpd.log'
self.ApacheAccessLogfile = '/var/log/apache2/lbb-access.log'
self.ApacheRcloneAccessLogfile = '/var/log/apache2/rclone-access.log'
self.proftpdAccessLogfile = '/var/log/proftpd/proftpd.log'

#objects
self.__setup = lib_setup.setup()
Expand Down Expand Up @@ -73,10 +73,10 @@ def check(self):
if ApacheRcloneLogfileAgeSec < IdleSecToPowerOff:
return(f'idletime: logfile rclone gui idletime not reached ({ApacheRcloneLogfileAgeSec}s < {IdleSecToPowerOff}s)')

# logfile vsftpd
vsftpdLogfileAgeSec = CompareTime - os.stat(self.vsftpdAccessLogfile).st_mtime if os.path.isfile(self.vsftpdAccessLogfile) else IdleSecToPowerOff
if vsftpdLogfileAgeSec < IdleSecToPowerOff:
return(f'idletime: logfile vsftpd idletime not reached ({vsftpdLogfileAgeSec}s < {IdleSecToPowerOff}s)')
# logfile proftpd
proftpdLogfileAgeSec = CompareTime - os.stat(self.proftpdAccessLogfile).st_mtime if os.path.isfile(self.proftpdAccessLogfile) else IdleSecToPowerOff
if proftpdLogfileAgeSec < IdleSecToPowerOff:
return(f'idletime: logfile proftpd idletime not reached ({proftpdLogfileAgeSec}s < {IdleSecToPowerOff}s)')

# check processes
for process in [
Expand Down
11 changes: 9 additions & 2 deletions scripts/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
$CloudServices_marked[] = 'cloud:' . $CloudService;
}

$LocalNetworkServices = array('ftp');

// rsync
$rsync_configurated = ($config['conf_RSYNC_SERVER']=='' or $config['conf_RSYNC_PORT']=='' or $config['conf_RSYNC_USER']=='' or $config['conf_RSYNC_PASSWORD']=='' or $config['conf_RSYNC_SERVER_MODULE']=='') == false;
if ($rsync_configurated) {
Expand All @@ -79,7 +81,7 @@
'anyusb' => $LocalAutoServices,
'usb' => $LocalServices,
'camera' => $CameraServices,
'cloud' => $CloudServices_marked
'cloud' => array_merge($CloudServices_marked, $LocalNetworkServices)
);

$TargetServices = array(
Expand Down Expand Up @@ -113,7 +115,8 @@ function HideDisallowedButtons(ActiveSource) {
if (
((TargetService === ActiveSource.value) && (TargetService !== 'usb')) ||
((ActiveSource.value === 'anyusb') && (TargetService === 'cloud_rsync')) ||
((ActiveSource.value === 'camera') && (TargetService === 'cloud_rsync'))
((ActiveSource.value === 'camera') && (TargetService === 'cloud_rsync')) ||
((ActiveSource.value === 'ftp') && (TargetService === 'cloud_rsync'))
) {
document.getElementById("Target_" + TargetService).disabled = true;
} else {
Expand Down Expand Up @@ -173,6 +176,10 @@ function HideDisallowedButtons(ActiveSource) {
elseif ($LabelName == 'cloud_rsync') {
$LabelName = l::box_backup_mode_cloud_rsync;
}
elseif ($LabelName == 'ftp') {
$LabelName = l::box_backup_mode_ftp;
}

print("<input type='radio' name='SourceDevice' value='$Storage' id='Source_$Storage' onchange='HideDisallowedButtons(this)' " . ($Storage == $OldSource ? 'checked' : '') . ">");
print("<label for='Source_$Storage'>$LabelName</label></br>");
}
Expand Down
6 changes: 5 additions & 1 deletion scripts/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,8 @@
"usbs": "USB-Speicher",
"nvme": "NVMe SSD",
"nvmes": "NVMe SSDs",
"rename": "Umbenennen"
"rename": "Umbenennen",
"ftp": "LBB's FTP-Server"
},
"nvme": "NVMe SSD",
"nvme_source": "USB-Quelle",
Expand All @@ -534,6 +535,9 @@
"mounting": {
"source": "Quelle einbinden",
"target": "Ziel einbinden"
},
"ftp": {
"started": "FTP-Server gestartet."
}
},
"cronip": {
Expand Down
6 changes: 5 additions & 1 deletion scripts/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@
"usbs": "USB storages",
"nvme": "NVMe SSD",
"nvmes": "NVMe SSDs",
"rename": "Rename"
"rename": "Rename",
"ftp": "LBB's FTP server"
},
"nvme": "NVMe SSD",
"nvme_source": "USB-source",
Expand All @@ -140,6 +141,9 @@
"mounting": {
"source": "Mount source",
"target": "Mount target"
},
"ftp": {
"started": "FTP server started."
}
},
"poweroff": {
Expand Down
6 changes: 5 additions & 1 deletion scripts/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,8 @@
"usbs": "Almacenamientos USB",
"nvme": "NVMe SSD",
"nvmes": "NVMe SSDs",
"rename": "Rebautizar"
"rename": "Rebautizar",
"ftp": "Servidor FTP de LBB"
},
"nvme": "NVMe SSD",
"nvme_source": "Fuente USB",
Expand All @@ -534,6 +535,9 @@
"mounting": {
"source": "Integrar fuente",
"target": "Integrar objetivo"
},
"ftp": {
"started": "Servidor FTP iniciado."
}
},
"cronip": {
Expand Down
6 changes: 5 additions & 1 deletion scripts/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,8 @@
"usbs": "Stockages USB",
"nvme": "NVMe SSD",
"nvmes": "NVMe SSDs",
"rename": "Rebaptiser"
"rename": "Rebaptiser",
"ftp": "Serveur FTP de LBB"
},
"nvme": "NVMe SSD",
"nvme_source": "Source USB",
Expand All @@ -534,6 +535,9 @@
"mounting": {
"source": "Monter la source",
"target": "Monter la cible"
},
"ftp": {
"started": "Le serveur FTP a démarré."
}
},
"cronip": {
Expand Down
Loading

0 comments on commit 313a38b

Please sign in to comment.