Утилиты sftp и lftp для обмена файлами

13.03.2023

Теги: CLIFTPLinuxPHPSSHДиректорияКомандаФайл

Протокол передачи файлов (FTP) был широко используемым протоколом для удаленной передачи файлов или данных в незашифрованном формате. Ему на смену пришел SFTP (Secure File Transfer Protocol), который работает по протоколу SSH на стандартном порту 22 для установления безопасного соединения.

Консольная утилита sftp

Подключиться к удаленному серверу по протоколу SFTP можно с помощью утилиты sftp

$ sftp username@server.com

Утилита sftp поддерживает множество опций. Опция -P позволяет указать порт. Опция -i — указать путь к файлу ssh-ключа. Опция -F — указать путь к файлу конфигурации ~/.ssh/config. Опция -b — передать пакетный файл с внутренними командами, которые надо выполнить на сервере после соединения.

Оказавшись в командной строке sftp можно получить список доступных внутренних команд с помощью команды help

> help
Available commands:
bye                                Quit sftp
cd path                            Change remote directory to 'path'
chgrp grp path                     Change group of file 'path' to 'grp'
chmod mode path                    Change permissions of file 'path' to 'mode'
chown own path                     Change owner of file 'path' to 'own'
df [-hi] [path]                    Display statistics for current directory or
                                   filesystem containing 'path'
exit                               Quit sftp
get [-afPpRr] remote [local]       Download file
reget [-fPpRr] remote [local]      Resume download file
reput [-fPpRr] [local] remote      Resume upload file
help                               Display this help text
lcd path                           Change local directory to 'path'
lls [ls-options [path]]            Display local directory listing
lmkdir path                        Create local directory
ln [-s] oldpath newpath            Link remote file (-s for symlink)
lpwd                               Print local working directory
ls [-1afhlnrSt] [path]             Display remote directory listing
lumask umask                       Set local umask to 'umask'
mkdir path                         Create remote directory
progress                           Toggle display of progress meter
put [-afPpRr] local [remote]       Upload file
pwd                                Display remote working directory
quit                               Quit sftp
rename oldpath newpath             Rename remote file
rm path                            Delete remote file
rmdir path                         Remove remote directory
symlink oldpath newpath            Symlink remote file
version                            Show SFTP version
!command                           Execute 'command' in local shell
!                                  Escape to local shell
?                                  Synonym for help

Поскольку пароль требуется вводить вручную — для использования sftp в скриптах потребуется утилита sshpass

$ sshpass -p 'qwerty' sftp -P 2222 username@server.com

Скачать файл или директорию

Можно сразу скачать файл с сервера, если указать путь к нему на сервере + указать локальный путь, куда это файл поместить

$ sshpass -p 'qwerty' sftp username@server.com:/remote/path/archive.zip /local/path
$ sshpass -p 'qwerty' sftp username@server.com:/remote/path/archive.zip /local/path/backup.zip

С использованием опции -r (рекурсивно) можно скачивать целые директории — например, директорию data

$ sshpass -p 'qwerty' sftp -r username@server.com:/remote/path/data /local/path/data

В примере выше директория data будет размещаться локально по пути /local/path/data/data. Но можно изменить второй аргумент команды sftp, не указывая директорию data — тогда локальный путь будет /local/path/data.

$ sshpass -p 'qwerty' sftp -r username@server.com:/remote/path/data /local/path

Выгрузить файл или директорию

Немного сложнее, если требуется не скачивать файлы с сервера, а выгружать их на сервер — синтаксис не очевиден.

$ echo 'put /local/path/archive.zip /remote/path' | sshpass -p 'qwerty' sftp username@server.com
$ echo 'put /local/path/archive.zip /remote/path/backup.zip' | sshpass -p 'qwerty' sftp username@server.com

С использованием опции -R (рекурсивно) для внутренней команды put можно выгружать целые директории

$ echo 'put -R /local/path/data /remote/path/data' | sshpass -p 'qwerty' sftp username@server.com

В примере выше директория data будет размещаться на сервере по пути /remote/path/data/data. Но можно изменить второй аргумент команды put, не указывая директорию data — тогда путь на сервере будет /remote/path/data.

$ echo 'put -R /local/path/data /remote/path' | sshpass -p 'qwerty' sftp username@server.com

Вообще, для скачивания и выгрузки файлов и директорий удобнее использовать одинаковый синтаксис — так проще запомнить.

$ echo 'get -R /remote/path/dir /local/path' | sshpass -p 'qwerty' sftp username@server.com
$ echo 'put -R /local/path/dir /remote/path' | sshpass -p 'qwerty' sftp username@server.com

Пакетный файл с командами

Опция -b позволяет передать пакетный файл с внутренними командами, которые надо выполнить на сервере после соединения. Давайте напишем скрипт, который будет удалять не пустую директорию на сервере. Для этого скрипт сначала скачивает с сервера директорию, которую нужно удалить. Потом проходит рекурсивно по локальной копии и записывает в файл все команды, которые нужно выполнить на сервере.

$ nano rmdir.sh
#!/bin/bash
# аргументом должен быть путь к директории
if [ $# -eq 0 ]; then
    echo 'Error! Empty directory name'
    exit 1
fi

# доступ к серверу: хост, пользователь, пароль
host='sftp.server.com'
user='sftp-user'
pass='qwerty'

# файл с внутренними командами для выполнения
batch='/home/evgeniy/sftp-server/batch.txt'
# директория на сервере, которую нужно удалить
dir=$1
# временная директория, куда скачаем директорию
temp='/home/evgeniy/sftp-server/temp'
rm -rf $temp/*

# скачиваем директорию, которую нужно удалить
response=$(echo "get -R $dir/. $temp" | sshpass -p $pass sftp $user@$host 2>&1)
if [[ $response =~ 'not found' ]]; then # директория не существует
    echo 'Remote directory not found'
    exit 1
fi

function commands {
    local file
    for file in $(ls $1); do
        if [ -d $1/$file ]; then
            commands $1/$file
            cmd="rmdir $1/$file"
        else
            cmd="rm $1/$file"
        fi
        # в строке $cmd заменяем подстроку $temp на подстроку $dir
        echo ${cmd/$temp/$dir} >> $batch
    done
}

echo -n '' > $batch
# пройдемся рекурсивно по содержимому локальной temp-директории, которая является копией
# директории на сервере — и запишем в файл команды, которые удалят содержимое на сервере
commands $temp
# последняя команда — удалить пустую директорию на сервере после удаления её содержимого
echo "rmdir $dir" >> $batch

# передаем пакетный файл с sftp-командами, которые удаляют директорию со всем содержимым
sshpass -p $pass sftp -oBatchMode=no -b $batch $user@$host > /dev/null 2>&1

cat $batch
rm $batch # удалим локальный файл с командами
rm -rf $temp/* # почистим временную директорию
$ chmod +x rmdir.sh

Консольная утилита lftp

Консольный клиент lftp может работать по протоколам FTP и SFTP + поддерживает расширенный набор команд по сравнению с утилитами ftp и sftp. Самое важное отличие — умеет удалять, скачивать и выгружать директории целиком.

Синтаксис

lftp [-d] [-e cmd] [-p port] [-u user[,pass]] [host|url]
lftp -f script_file
lftp -c commands
lftp --version
lftp --help

Справка

$ lftp --help
Использование: lftp [опции] <адрес>
    -f <файл>           выполнить команды из указанного файла и выйти
    -c <команда>        выполнить команду и выйти
    --norc              не выполнять rc-файлы из домашнего каталога
    --help              вывести данную подсказку и выйти
    --version           вывести информацию о версии и выйти
Остальные ключи аналогичны ключам команды open
    -e <команда>        выполнить команду после выбора сервера
    -u <имя>[,<пароль>] использовать для аутентификации имя/пароль
    -p <порт>           использовать для соединения указанный порт
    -s <слот>           перейти в указанный слот
    -d                  включить отладочный режим
    <адрес>             имя сервера, URL или имя закладки

Сеанс работы

$ lftp
> open ftp://username:password@server.com # установить соединение
> ls # список файлов в текущей директории
drwxrwxr-x    4 1001     1002         4096 Mar 15 11:26 .
drwxr-xr-x    3 root     root         4096 Mar 11 08:53 ..
drwxr-xr-x    3 1001     1002         4096 Mar 15 11:26 data
> cd data # перейти в директорию data
> ls # список файлов в текущей директории
drwxr-xr-x    3 1001     1002         4096 Mar 15 11:26 .
drwxrwxr-x    4 1001     1002         4096 Mar 15 11:26 ..
drwxr-xr-x    2 1001     1002         4096 Mar 15 11:26 images
-rw-r--r--    1 1001     1002        39030 Mar 15 11:26 import.xml
-rw-r--r--    1 1001     1002        10826 Mar 15 11:26 offers.xml
> mget import.xml offers.xml # скачать два файла
49856 байтов перемещено
Всего перемещено: 2 файла
> put archive.zip # выгрузить один файл
7620 байтов перемещено
> exit # завершить сеанс работы

Чтобы работать по протоколу SFTP вместо FTP — заменяем ftp://server.com на sftp://server.com.

По умолчанию вывод команды ls кэшируется, чтобы увидеть новый список, нужно выполнить команду rels или cache flush.

Опции -c и -e позволяют выполнить несколько внутренних команд. Отличие между ними в том, что -c — опция утилиты lftp, а -e — опция команды open (при этом допускается ее использование с lftp). Это значит, что при использовании -c нужно сперва установить соединение с помощью open, а при использовании -e — соединение уже должно быть установлено.

$ lftp -c 'open ftp://username:password@server.com; get /remote/path/data.zip; put /local/path/price.csv'
$ lftp -e 'get /remote/path/data.zip; put /local/path/price.csv; exit' ftp://username:password@server.com

При использовании -c, после выполнения команд, утилита lftp завершает работу. При использовании -e, после выполнения команд, работа с сервером продолжается — если не использовать exit в конце.

Файлы конфигурации утилиты

При запуске lftp выполняет внутренние команды из файла конфигурации /etc/lftp.conf, а затем — из файлов ~/.lftprc и ~/.lftp/rc (или из ~/.config/lftp/rc, если ~/.lftp не существует).

$ nano ~/.config/lftp/rc
set ftp:ssl-allow no

Настройки можно посмотреть, используя внутреннюю команду set с опцией -a или -d.

$ lftp
> set -a # значения всех настроек, в том числе значения по умолчанию
> set -d # значения настроек по умолчанию, могут быть перезаписаны

Некоторые внутренние команды

Команда mkdir позволяет создать одну или несколько директорий на сервере.

> mkdir каталог(и)

Команда rmdir позволяет удалить пустую директорию или директории на сервере.

> rmdir каталог(и)

Команда rm позволяет удалить файлы на сервере. Опция -r нужна для рекурсивного удаления каталога. Опция -f подавляет сообщения об ошибках.

> rm [-r] [-f] файл(ы)

Команда get позволяет скачать файл с сервера. Опция -E удаляет файл на сервере после скачивания. Опция -o задает локальное имя файла.

> get [-E] файл [-o файл]

Команда mget позволяет скачать несколько файлов с сервера. Опция -E удаляет файлы на сервере после скачивания.

$ mget [-E] файлы

Команда put позволяет выгрузить файл на сервер. Опция -E удаляет локальный файл после выгрузки. Опция -o задает имя файла на сервере.

> put [-E] файл [-o файл]

Команда mput позволяет выгрузить несколько файлов на сервер. Опция -E удаляет локальные файлы после выгрузки.

$ mput [-E] файлы

Внутренняя команда mirror

Утилита lftp имеет встроенную команду mirror, которая позволяет скачать с сервера директорию целиком. Существует также обратное зеркало mirror -R — для выгрузки на сервер директории целиком.

> mirror [опции] [source [target[/]]]
$ lftp -e 'mirror [опции] [source [target[/]]]; exit' ftp://username:password@server.com

Обратите внимание на слэш в конце target — без него файлы источника /path/to/source/файлы будут размещены в /path/to/target/файлы. А при наличии слэша в конце target — файлы будут в /path/to/target/source/файлы, что скорее всего не нужно.

Скачать удаленную директорию /remote/folder в локальную директорию /local/folder

$ lftp -e 'mirror /remote/folder /local/folder; exit' sftp://username:password@server.com
$ lftp -e 'mirror /remote/folder /local/folder; exit' -u username,password sftp://server.com

Скачать удаленную директорию /remote/folder в локальную директорию /local/folder, удалить в /local/folder файлы, которых нет на сервере

$ lftp -e 'mirror --delete /remote/folder /local/folder; exit' sftp://username:password@server.com
$ lftp -e 'mirror --delete /remote/folder /local/folder; exit' -u username,password sftp://server.com

Выгрузить локальную директорию /local/folder в удаленную директорию /remote/folder

$ lftp -e 'mirror --reverse /local/folder /remote/folder; exit' sftp://username:password@server.com
$ lftp -e 'mirror --reverse /local/folder /remote/folder; exit' -u username,password sftp://server.com

Выгрузить локальную директорию /local/folder в удаленную директорию /remote/folder, удалить в /remote/folder файлы, которых нет локально

$ lftp -e 'mirror --reverse --delete /local/folder /remote/folder; exit' sftp://username:password@server.com
$ lftp -e 'mirror --reverse --delete /local/folder /remote/folder; exit' -u username,password sftp://server.com

Обратите внимание, что источник всегда является первым аргументом команды mirror, а приемник — вторым.

Некторые опции команды mirror

  • --continue — продолжить передачу после сбоя, если это возможно
  • --delete — удалить файлы в приемнике, если их нет в источнике
  • --only-missing — передавать только файлы, которых еще нет в приемнике
  • --only-existing — передавать только файлы, которые уже есть в приемнике
  • --Remove-source-files — удалить файлы источника после передачи (осторожно)
  • --Remove-source-dirs — удалить файлы и директории источника после передачи
  • --verbose=0,1,2,3 — вывод сообщений о ходе выполнения команды

Пакетный файл с командами

Можно записать в файл последовательность внутренних команд, которые нужно выполнить на сервере — и передать этот файл утилите lftp.

$ nano exchange.txt
open sftp://username:password@server.com
mirror --delete /remote/path/data /local/path/data
cd /remote/path/csv
put /local/path/csv/price.csv
lcd /local/path/xml
mget /remote/path/xml/import.xml /remote/path/xml/offers.xml
$ lftp -f exchange.txt

Примеры использования утилит

По работе нужно было написать скрипт, который бы запускался по расписанию и забирал с файлы с облачного хранилища. Использовал PHP, реализовал два варианта — первый использует sftp, а второй — работает с lftp.

#!/usr/bin/php
<?php
$SERVER_USER = 'sftp-user';
$SERVER_HOST = 'sftp.server.com';
$SERVER_PASS = 'qwerty';
// файлы могут быть расположены в корневой директории сервера, тогда нужно использовать значение /
// или файлы могут быть расположены уровнем ниже, тогда нужно использовать значение /path/to/data
$SERVER_PATH = '/sftp-user';

$TELEGRAM_TOKEN = '..........';
$TELEGRAM_CHATID = '.........';

$WORK_DIR = '/home/evgeniy/exchange';
$REPORT = [];
$ERRORS = [];
$DEBUG = true;

/*
Раз в сутки нужно получать файлы от контрагентов и сохранять их в сетевую папку для дальнейшей обработки
в 1C:Предприятие. После успешного получения и сохранения — нужно удалить файлы в облачном хранилище. Как
сигнал того, что файлы успешно получены и хранилище готово к приему новых файлов от контрагентов. Кол-во
контрагентов может меняться — в хранилище будут появляться новые директории и пропадать старые.

1. получаем содержимое директории каждого клиента из хранилища
2. проверяем, что от каждого клиента получен хотя бы один файл
3. очищаем содержимое директории каждого клиента в хранилище
4. проверяем, что директории всех клиентов в хранилище пустые
*/

run();

if (!empty($REPORT)) {
    telegram('Обмен с облачным хранилищем, есть проблемы' . PHP_EOL . implode(PHP_EOL, $REPORT));
} else {
    telegram('Обмен с облачным хранилищем прошел успешно');
}
if ($DEBUG && !empty($ERRORS)) {
    telegram('DEBUG: ошибки обмена с облачным хранилищем' . PHP_EOL . implode(PHP_EOL, $ERRORS));
}

function run() {
    global $REPORT;

    // 1. получаем содержимое директории каждого клиента из хранилища
    $result = pull_folders();
    if (!$result) { // нельзя продолжать, если файлы не получены
        $REPORT[] = 'Ошибки получения файлов из хранилища';
        return;
    }

    // 2. проверяем, что от каждого клиента получен хотя бы один файл
    $result = check_folders();
    if (!$result) {
        $REPORT[] = 'От некоторых клиентов файлы не получены';
    }

    // 3. очищаем содержимое директории каждого клиента в хранилище
    $result = clear_server();
    if (!$result) {
        $REPORT[] = 'Ошибки удаления файлов клиентов в хранилище';
    }

    // 4. проверяем, что директории всех клиентов в хранилище пустые
    $result = check_server();
    if (!$result) {
        $REPORT[] = 'Некоторые файлы не удалены в хранилище';
    }
}

// 1. ф-ция получает содержимое директории каждого клиента из хранилища
function pull_folders() {
    global $WORK_DIR, $SERVER_HOST, $SERVER_USER, $SERVER_PASS, $SERVER_PATH, $ERRORS;

    // сначала удаляем все локальные директории клиентов
    clear_folder($WORK_DIR . '/input/');
    // потом получаем из хранилища все директории клиентов
    $REMOTE_PATH = $SERVER_PATH === '/' ? '/' : $SERVER_PATH . '/.';
    $command = "get -R ${REMOTE_PATH} ${WORK_DIR}/input";
    $sftp = "echo '${command}' | sshpass -p '${SERVER_PASS}' sftp ${SERVER_USER}@${SERVER_HOST} 2>&1";
    $output = $return = null;
    exec($sftp, $output, $return);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    // изменяем права доступа для директорий и файлов
    if (!chmod_folder($WORK_DIR . '/input')) return false;
    return true;
}

// 2. ф-ция проверяет, что от каждого клиента получен хотя бы один файл
function check_folders() {
    global $WORK_DIR, $ERRORS;
    $dirs = scandir($WORK_DIR . '/input');
    foreach ($dirs as $dir) {
        if ($dir === '.' || $dir === '..') continue;
        $items = scandir($WORK_DIR . '/input/' . $dir);
        if (count($items) === 2) {
            return false;
        }
    }
    return true;
}

// 3. ф-ция очищает содержимое директории каждого клиента в хранилище
function clear_server() {
    global $WORK_DIR, $SERVER_HOST, $SERVER_USER, $SERVER_PASS, $SERVER_PATH, $ERRORS;

    $filename = $WORK_DIR . '/temp/commands.txt';
    if (is_file($filename)) unlink($filename);
    // формируем массив sftp команд для удаления файлов хранилища
    $dirs = scandir($WORK_DIR . '/input');
    $commands = [];
    foreach ($dirs as $dir) {
        if ($dir === '.' || $dir === '..') continue;
        $commands = array_merge($commands, commands($WORK_DIR . '/input/' . $dir));
    }
    if (empty($commands)) return true;

    // команды записываем в файл и отправяем пакетом для выполнения
    file_put_contents($WORK_DIR . '/temp/commands.txt', implode(PHP_EOL, $commands));
    $sftp = "sshpass -p '${SERVER_PASS}' sftp -oBatchMode=no -b ${filename} ${SERVER_USER}@${SERVER_HOST} 2>&1";
    $output = $return = null;
    exec($sftp, $output, $return);
    unlink($filename);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    return true;
}

// 4. ф-ция проверяет, что директории всех клиентов в хранилище пустые
function check_server() {
    global $WORK_DIR, $SERVER_HOST, $SERVER_USER, $SERVER_PASS, $SERVER_PATH, $ERRORS;

    // загружаем из хранилища все директории клиентов...
    if (!is_dir($WORK_DIR . '/temp/check')) mkdir($WORK_DIR . '/temp/check');
    clear_folder($WORK_DIR . '/temp/check');
    $REMOTE_PATH = $SERVER_PATH === '/' ? '/' : $SERVER_PATH . '/.';
    $command = "get -R ${REMOTE_PATH} ${WORK_DIR}/temp/check";
    $sftp = "echo '${command}' | sshpass -p '${SERVER_PASS}' sftp ${SERVER_USER}@${SERVER_HOST} 2>&1";
    $output = $return = null;
    exec($sftp, $output, $return);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    // изменяем права доступа для директорий и файлов
    if (!chmod_folder($WORK_DIR . '/temp/check')) return false;
    // ...проверяем их содержимое, они должны быть пустые
    $success = true;
    $dirs = scandir($WORK_DIR . '/temp/check');
    foreach ($dirs as $dir) {
        if ($dir === '.' || $dir === '..') continue;
        $items = scandir($WORK_DIR . '/temp/check/' . $dir);
        if (count($items) > 2) { // в директории клиента что-то осталось
            $success = false;
            break;
        }
    }

    clear_folder($WORK_DIR . '/temp/check');
    rmdir($WORK_DIR . '/temp/check');
    return $success;
}

// рекурсивно удаляет директории и файлы внутри указанной локальной директории
function clear_folder($dir) {
    $items = scandir($dir);
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        if (is_dir($dir . '/' . $item)) {
            clear_folder($dir . '/' . $item);
            rmdir($dir . '/' . $item);
        } else {
            unlink($dir . '/' . $item);
        }
    }
}

// рекурсивно устанавливает права на файлы внутри указанной локальной директории
function chmod_folder($dir) {
    global $ERRORS;
    // изменяем права доступа для директорий
    $chmod = "find ${dir} -type d -exec chmod 755 {} + 2>&1";
    $output = $return = null;
    exec($chmod, $output, $return);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    // изменяем права доступа для файлов
    $chmod = "find ${dir} -type f -exec chmod 644 {} + 2>&1";
    $output = $return = null;
    exec($chmod, $output, $return);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    return true;
}

// возвращает массив команд sftp для удаления директорий и файлов внутри директории
// хранилища; рекурсивный обход делаем на локальной копии директории хранилища
function commands($dir) {
    global $WORK_DIR, $SERVER_PATH;
    $files = [];
    $replace = $SERVER_PATH === '/' ? '/' : $SERVER_PATH . '/';
    $items = scandir($dir);
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        if (is_dir($dir . '/' . $item)) {
            $files = array_merge($files, commands($dir . '/' . $item));
            $files[] = str_replace($WORK_DIR . '/input/', $replace, "rmdir ${dir}/${item}");
        } else {
            $files[] = str_replace($WORK_DIR . '/input/', $replace, "rm ${dir}/${item}");
        }
    }
    return $files;
}

function telegram($message) {
    global $TELEGRAM_TOKEN, $TELEGRAM_CHATID;
    $data = http_build_query([
        'chat_id' => $TELEGRAM_CHATID,
        'text' => $message
    ]);
    $options = [
        'http' => [
            'method'  => 'POST',
            'header'  => 'Content-type: application/x-www-form-urlencoded',
            'content' => $data
        ]
    ];
    $context  = stream_context_create($options);
    $response = file_get_contents("https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage", false, $context);
}
#!/usr/bin/php
<?php
$SERVER_USER = 'sftp-user';
$SERVER_HOST = 'sftp.server.com';
$SERVER_PASS = 'qwerty';
// файлы могут быть расположены в корневой директории сервера, тогда нужно использовать значение /
// или файлы могут быть расположены уровнем ниже, тогда нужно использовать значение /path/to/data
$SERVER_PATH = '/sftp-user';
$SERVER_PROT = 'sftp'; // можно работать по ftp или sftp
$DISABLE_SSL = $SERVER_PROT === 'ftp' ? 'set ftp:ssl-allow no; ' : '';

$TELEGRAM_TOKEN = '..........';
$TELEGRAM_CHATID = '.........';

$WORK_DIR = '/home/evgeniy/exchange';
$REPORT = [];
$ERRORS = [];
$DEBUG = true;

/*
Раз в сутки нужно получать файлы от контрагентов и сохранять их в сетевую папку для дальнейшей обработки
в 1C:Предприятие. После успешного получения и сохранения — нужно удалить файлы в облачном хранилище. Как
сигнал того, что файлы успешно получены и хранилище готово к приему новых файлов от контрагентов. Кол-во
контрагентов может меняться — в хранилище будут появляться новые директории и пропадать старые.

1. получаем содержимое директории каждого клиента из хранилища
2. проверяем, что от каждого клиента получен хотя бы один файл
3. очищаем содержимое директории каждого клиента в хранилище
4. проверяем, что директории всех клиентов в хранилище пустые
*/

run();

if (!empty($REPORT)) {
    telegram('Обмен с облачным хранилищем, есть проблемы' . PHP_EOL . implode(PHP_EOL, $REPORT));
} else {
    telegram('Обмен с облачным хранилищем прошел успешно');
}
if ($DEBUG && !empty($ERRORS)) {
    telegram('DEBUG: ошибки обмена с облачным хранилищем' . PHP_EOL . implode(PHP_EOL, $ERRORS));
}

function run() {
    global $REPORT;

    // 1. получаем содержимое директории каждого клиента из хранилища
    $result = pull_folders();
    if (!$result) { // нельзя продолжать, если файлы не получены
        $REPORT[] = 'Ошибки получения файлов из хранилища';
        return;
    }

    // 2. проверяем, что от каждого клиента получен хотя бы один файл
    $result = check_folders();
    if (!$result) {
        $REPORT[] = 'От некоторых клиентов файлы не получены';
    }

    // 3. очищаем содержимое директории каждого клиента в хранилище
    $result = clear_server();
    if (!$result) {
        $REPORT[] = 'Ошибки удаления файлов клиентов в хранилище';
    }

    // 4. проверяем, что директории всех клиентов в хранилище пустые
    $result = check_server();
    if (!$result) {
        $REPORT[] = 'Некоторые файлы не удалены в хранилище';
    }
}

// 1. ф-ция получает содержимое директории каждого клиента из хранилища
function pull_folders() {
    global $WORK_DIR, $SERVER_PROT, $DISABLE_SSL, $SERVER_HOST, $SERVER_USER, $SERVER_PASS, $SERVER_PATH, $ERRORS;

    $open = "open ${SERVER_PROT}://${SERVER_USER}:'${SERVER_PASS}'@${SERVER_HOST}";
    // скопировать содержимое директорий всех клиентов, удалить локально не существующие в хранилище
    // файлы (--delete), докачать файлы, если были проблемы с сетью во время передачи (--continue)
    $mirror = "mirror --delete --continue ${SERVER_PATH} ${WORK_DIR}/input";
    $lftp = "lftp -c '${DISABLE_SSL}${open}; ${mirror}' 2>&1";
    $output = $return = null;
    exec($lftp, $output, $return);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    // изменяем права доступа для директорий и файлов
    if (!chmod_folder($WORK_DIR . '/input')) return false;
    return true;
}

// 2. ф-ция проверяет, что от каждого клиента получен хотя бы один файл
function check_folders() {
    global $WORK_DIR, $ERRORS;
    $dirs = scandir($WORK_DIR . '/input');
    foreach ($dirs as $dir) {
        if ($dir === '.' || $dir === '..') continue;
        $items = scandir($WORK_DIR . '/input/' . $dir);
        if (count($items) === 2) {
            return false;
        }
    }
    return true;
}

// 3. ф-ция очищает содержимое директории каждого клиента в хранилище
function clear_server() {
    global $WORK_DIR, $SERVER_PROT, $DISABLE_SSL, $SERVER_HOST, $SERVER_USER, $SERVER_PASS, $SERVER_PATH, $ERRORS;

    if (!is_dir($WORK_DIR . '/temp/empty')) mkdir($WORK_DIR . '/temp/empty');
    clear_folder($WORK_DIR . '/temp/empty');

    $mirrors = [];
    // $SERVER_PATH может быть / или /path/to/data, нужно избежать ситуации //client-data-dir
    $REMOTE_PATH = $SERVER_PATH === '/' ? '/' : $SERVER_PATH . '/';
    $dirs = scandir($WORK_DIR . '/input');
    foreach ($dirs as $dir) {
        if ($dir === '.' || $dir === '..') continue;
        // просто выгружаем в директорию каждого клиента временно созданную пустую директорию
        $mirrors[] = "mirror --reverse --delete ${WORK_DIR}/temp/empty ${REMOTE_PATH}${dir}";
    }
    if (empty($mirrors)) return false;

    $success = true;
    $open = "open ${SERVER_PROT}://${SERVER_USER}:'${SERVER_PASS}'@${SERVER_HOST}";
    $mirrors = implode('; ', $mirrors);
    $lftp = "lftp -c '${DISABLE_SSL}${open}; ${mirrors}' 2>&1";
    $output = $return = null;
    exec($lftp, $output, $return);
    if ($return !== 0) {
        $success = false;
        $ERRORS[] = implode(' ', $output);
    }

    clear_folder($WORK_DIR . '/temp/empty');
    rmdir($WORK_DIR . '/temp/empty');
    return $success;
}

// 4. ф-ция проверяет, что директории всех клиентов в хранилище пустые
function check_server() {
    global $WORK_DIR, $SERVER_PROT, $DISABLE_SSL, $SERVER_HOST, $SERVER_USER, $SERVER_PASS, $SERVER_PATH, $ERRORS;

    // загружаем из хранилища все директории клиентов...
    if (!is_dir($WORK_DIR . '/temp/check')) mkdir($WORK_DIR . '/temp/check');
    clear_folder($WORK_DIR . '/temp/check');
    $open = "open ${SERVER_PROT}://${SERVER_USER}:'${SERVER_PASS}'@${SERVER_HOST}";
    $mirror = "mirror --delete ${SERVER_PATH} ${WORK_DIR}/temp/check";
    $lftp = "lftp -c '${DISABLE_SSL}${open}; ${mirror}' 2>&1";
    $output = $return = null;
    exec($lftp, $output, $return);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    // изменяем права доступа для директорий и файлов
    if (!chmod_folder($WORK_DIR . '/temp/check')) return false;
    // ...проверяем их содержимое, они должны быть пустые
    $success = true;
    $dirs = scandir($WORK_DIR . '/temp/check');
    foreach ($dirs as $dir) {
        if ($dir === '.' || $dir === '..') continue;
        $items = scandir($WORK_DIR . '/temp/check/' . $dir);
        if (count($items) > 2) {
            $success = false;
            break;
        }
    }

    clear_folder($WORK_DIR . '/temp/check');
    rmdir($WORK_DIR . '/temp/check');
    return $success;
}

// рекурсивно удаляет директории и файлы внутри указанной локальной директории
function clear_folder($dir) {
    $items = scandir($dir);
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        if (is_dir($dir . '/' . $item)) {
            clear_folder($dir . '/' . $item);
            rmdir($dir . '/' . $item);
        } else {
            unlink($dir . '/' . $item);
        }
    }
}

// рекурсивно устанавливает права на файлы внутри указанной локальной директории
function chmod_folder($dir) {
    global $ERRORS;
    // изменяем права доступа для директорий
    $chmod = "find ${dir} -type d -exec chmod 755 {} + 2>&1";
    $output = $return = null;
    exec($chmod, $output, $return);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    // изменяем права доступа для файлов
    $chmod = "find ${dir} -type f -exec chmod 644 {} + 2>&1";
    $output = $return = null;
    exec($chmod, $output, $return);
    if ($return !== 0) {
        $ERRORS[] = implode(' ', $output);
        return false;
    }
    return true;
}

function telegram($message) {
    global $TELEGRAM_TOKEN, $TELEGRAM_CHATID;
    $data = http_build_query([
        'chat_id' => $TELEGRAM_CHATID,
        'text' => $message
    ]);
    $options = [
        'http' => [
            'method'  => 'POST',
            'header'  => 'Content-type: application/x-www-form-urlencoded',
            'content' => $data
        ]
    ];
    $context  = stream_context_create($options);
    $response = file_get_contents("https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage", false, $context);
}

Поиск: CLI • FTP • Linux • PHP • SSH • Директория • Команда • Файл

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.