Гость
Целевая тема:
Создать новую тему:
Автор:
Форумы / Публикации участников [закрыт для гостей] / Скрипты: Бэкапы на Яндекс Диск через REST API / 7 сообщений из 7, страница 1 из 1
20.10.2025, 13:04
    #32
basename
Модератор форума
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Скрипты: Бэкапы на Яндекс Диск через REST API
В рамках поддержки этого и параллельного форумов возникла задача резервного копирования баз дынных, вложений и системных конфигураций. На момент написания этой заметки общий рабочий объём БД всех форумов примерно 160 ГБ и примерно 120 ГБ вложений.

Так как используется сервис Я 360 для работы электронной почты, то логично воспользоваться доступным пространством Яндекс Диска 1 ТБ на пользователя для осуществления задачи резервного копирования данных.

Я искал способы, как туда можно копировать архивы, пробовал официальную программу, доступную в консольном варианте, однако функционирование программы не понравилось: программа очевидно предназначена для пользователя и сделана явным образом максимально неудобно для автоматизации какой-либо инфраструктурной задачи, оно и понятно, тут претензий нет, компания коммерческая, хотите инфраструктурный сервис - платите соответствующие деньги. Извернуться с программой можно, но очень неудобно.

Случайным образом было найдено решение здесь https://neblog.info/skript-bekapa-na-yandeks-disk?ysclid=lqaufsd6g9584100748 - простой скрипт бэкапа штатными средствами ОС и публикация бэкапов на Яндекс Диск через API. То, что надо! Я и не знал до этого, что Яндекс свободно доступ к API диска предоставляет.

Скрипт данного автора меня тоже не совсем устроил, прежде всего потому, что он там всё в один фалй архивирует как проект, а я этого в принципе сделать не могу из-за объема. Но это мелочи, главное, что там приводятся готовые методы работы с API и не надо писать самому, подправить то легче.

Соответственно, приведу примеры используемых скриптов для резервного копирования БД и файлов. Скрипт максимально универсален, подправить для себя не составит сложностей.

Что может скрипт:
- делает бекапы штатными системными средствами
- архивирует, выбор сжатия gzip и xz
- выкладывает архивы на Яндекс.Диск через API
- пишет лог и отправляет уведомления на почту
- удаляет локальные архивные файлы бэкапов после выгрузки на Яндекс.Диск
- может удалять файлы в Яндекс.Диск старше чем за указанный период

Нюанс: Яндекс ограничивает скорость загрузки для некоторых MIME https://yadisk.readthedocs.io/ru/dev/known_issues.html. Чтобы не ограничивал - достаточно добавить кастомное расширение файла. При необходимости восстановления расширение достаточно удалить и распаковать как обычный архив.

Немного замечаний по использованию.

Прежде всего нужно создать и зарегистрировать собственное приложение в Яндекс и получить токен. Зарегистрировать приложение необходимо именно по этой ссылке https://oauth.yandex.ru/client/new . Если регистрировать непосредственно из интерфейса Яндекс, то там нет возможности задать приложению права на использование REST API (зачем так сделали - не ясно). После регистрации приложения в Яндекс.Диск в корне появится директория Applications c вашим приложением. Туда и будут файлы загружаться при использовании API.Документация по API Яндекс Диска - https://yandex.ru/dev/disk-api/doc/ru/

Примеры скриптов. Для БД и архивирование файлов.

Алгоритм работы:
- подготовка архивов - отработка по циклу for... В случае БД просто перечисляю две БД подряд, в случае архивирования директорий - задаю их как переменные, при этом использую разделитель : для именований и далее как с БД, переменные в цикл for...
- выгрузка архивов
- удаление локальных временных архивов и устаревших на Я.Диск

Я полностью сохранил оригинальные комментарии автора в скрипте, ссылку на оригинальный источник + мои исправления и дополнения

Бэкап БД (дамп)
Код: BASH
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
#!/bin/bash
#
# Yandex.Disk backup script v1.0 by Sergey Lukonin (neblog.info) // modified by Anton Koudinov (itwrks.org)
#
#

# # # # # # # # # # НАСТРОЙКИ БЭКАПА MYSQL # # # # # # # # # #

#
# В домашней директории пользователя, из под которого будет запускаться программа mysqldump, создайте конфигурационный файл ~/.my.cnf с параметрами доступа к серверу MYSQL следующего содержания:
#
## [mysqldump]
## host = 127.0.0.1
## user = backup
## password= "backup123"
#

# Параметры mysqldump
MYSQLDUMP_PARAMS='--routines --triggers --single-transaction --quick --max_allowed_packet=64M --lock-tables=false --complete-insert'

# # # # # # # # # # ОБЩИЕ НАСТРОЙКИ # # # # # # # # # #

# Директория для временного хранения бэкапов, которые удаляются после отправки на Яндекс.Диск
BACKUP_DIR='/mnt/localfs/nfs/share/_backup/upload/DB'

# Название проекта, используется в логах и именах архивов
PROJECT='itwrks.org'

# Сколько дней хранятся бэкапы на Яндекс.Диске (0 - хранить все бэкапы). Учитывайте расписание резервного копирования, так как дата для автоматического удаления рассчитывается от текущей минус количество дней, указанных в параметре.
# Например, делаете архивы каждый день и хотите хранить за 14 дней - задаёте 14. Делаете еженедельно и хотите хранить 4 архива - задаёте 30
MAX_BACKUPS='7'

# Базы данных для архивации (указываются через пробел), которые будут отправлены на Яндекс.Диск
DBS='NOSQLRU RESQLRU RED_FORUM'

# Yandex.Disk токен (как получить - см. на neblog.info)
TOKEN='123qweasdzxc'

# Директория приложения на Яндекс.Диске, куда будут отправлены архивы. Необходимо создать предварительно
APP_DIR='DB'

# Произвольное имя расширения (с точкой!) для архива (Яндекс ограничивает скорость загрузки для некоторых MIME https://yadisk.readthedocs.io/ru/dev/known_issues.html. При необходимости восстановления расширение достаточно удалить и распаковать как обычный архив)
ext='.yarch'

# Имя лог-файла, хранится в директории, указанной в $BACKUP_DIR
LOGFILE='backup.log'

# E-mail для отправки результата выполнения скрипта. Оставьте пустым, если отправлять результаты не требуется
sendLog='root@itwrks.org'

# Отправлять только ошибки (true). Укажите false, если нужно отправлять логи при любом результате выполнения скрипта
sendLogErrorsOnly='false'

# Выбор сжатия. xz - сильнее сжатие, меленнее скорость. gzip - слабее сжатие, быстрее скорость. Оптимальный вариант - gzip
arch='gzip'

# # # # # # # # # # КОНЕЦ НАСТРОЕК # # # # # # # # # # # # #
# # # # # # # # ДАЛЬШЕ НИЧЕГО НЕ МЕНЯЕМ! # # # # # # # # # #

# Дата, используется в именах архивов для расчёта количества бэкапов для автоматического удаления
DATE="`date '+%Y%m%d'`.`date '+%H%M%S'`"

# # # # # # # # ДАЛЬШЕ НИЧЕГО НЕ МЕНЯЕМ! # # # # # # # # # #

cd $BACKUP_DIR || exit 88

function mailing()
{
    if [ ! $sendLog = '' ];then
        if [ "$sendLogErrorsOnly" == true ];
        then
            if echo "$1" | grep -q 'error'
            then
                echo "$2" | mail -s "$1" $sendLog > /dev/null
            fi
        else
            echo "$2" | mail -s "$1" $sendLog > /dev/null
        fi
    fi
}

function logger()
{
    echo "["`date "+%Y-%m-%d %H:%M:%S"`"] File $BACKUP_DIR: $1" >> $BACKUP_DIR/$LOGFILE
}

function parseJson()
{
    local output
    regex="(\"$1\":[\"]?)([^\",\}]+)([\"]?)"
    [[ $2 =~ $regex ]] && output=${BASH_REMATCH[2]}
    echo $output
}

function checkError()
{
    echo $(parseJson 'error' "$1")
}

function getUploadUrl()
{
    json_out=`curl -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources/upload/?path=app:/$APP_DIR/$backupName&overwrite=true"`
    json_error=$(checkError "$json_out")
    if [[ $json_error != '' ]];
    then
        logger "$PROJECT - Yandex.Disk error: $json_error"
        mailing "$PROJECT - Yandex.Disk backup error" "ERROR copy file $FILENAME. Yandex.Disk error: $json_error"
    echo ''
    else
        output=$(parseJson 'href' $json_out)
        echo $output
    fi
}

function uploadFile
{
    local json_out
    local uploadUrl
    local json_error
    uploadUrl=$(getUploadUrl)
    if [[ $uploadUrl != '' ]];
    then
    echo $UploadUrl
        json_out=`curl -s -T $1 -H "Authorization: OAuth $TOKEN" $uploadUrl`
        json_error=$(checkError "$json_out")
    if [[ $json_error != '' ]];
    then
        logger "$PROJECT - Yandex.Disk error: $json_error"
        mailing "$PROJECT - Yandex.Disk backup error" "ERROR copy file $FILENAME. Yandex.Disk error: $json_error"

    else
        logger "$PROJECT - Copying file to Yandex.Disk success"
        mailing "$PROJECT - Yandex.Disk backup success" "SUCCESS copy file $FILENAME"

    fi
    else
        echo 'Some errors occured. Check log file for detail'
    fi
}

function backups_list() {
    # Ищем в директории приложения все файлы бэкапов и выводим их названия:
    curl -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$APP_DIR/&sort=created&limit=100" | tr "{},[]" "\n" | grep "name[[:graph:]]*.sql.*$ext" | cut -d: -f 2 | tr -d '"' | grep -v ^http
}

function backups_count() {
    local bkps=$(backups_list | wc -l)
    echo $bkps
}

function remove_old_backups() {
    logger "Удаляем старые бэкапы с Яндекс.Диска"
    # Цикл удаления старых бэкапов:
    # - От текущей даты отсчитываем количество дней, указанных в параметре MAX_BACKUPS и определяем дату для удаления
    # - Основываясь на имени файла, удаляем бэкапы до необходимой даты
    bkps=$(backups_count)
    if [ $bkps -gt 0 ]; then
        old_bkps=$(date '+%Y%m%d' -d "-$MAX_BACKUPS day")
        while read entry; do
            old_entry=$(echo $entry | cut -f1 -d.)
            if [ $old_entry -lt $old_bkps ]; then
                logger "Удаляем $entry"
                curl -X DELETE -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$APP_DIR/$(backups_list | awk '(NR == 1)')&permanently=true"
            fi
        done <<< `backups_list`
    fi
}

logger "--- $PROJECT START BACKUP $DATE ---"

logger "Делаем дамп баз данных"

# Определяем расширение файла в зависомости от выбора архиватора
[ $arch = 'gzip' ] && ext_arch='gz'
[ $arch = 'xz' ] && ext_arch='xz'

for DB in $DBS
    do
        logger "Делаем дампы базы данных $DB"
        mysqldump $MYSQLDUMP_PARAMS $DB | $arch -c > $BACKUP_DIR/$DATE.$DB.sql.$ext_arch$ext
    done

for file in `ls $BACKUP_DIR --hide=$LOGFILE`
    do
        FILENAME=$file
        logger "Выгружаем на Яндекс.Диск архив $file"
        backupName=$file
        uploadFile $file
    done

logger "Удаляем архивы с диска"
find $BACKUP_DIR -maxdepth 1 -type f -name "*.sql.*$ext" -exec rm '{}' \;

# Удаляем старые бэкапы с Яндекс.Диска (если MAX_BACKUPS > 0)
if [ $MAX_BACKUPS -gt 0 ]; then remove_old_backups; fi

logger "Завершение скрипта бэкапа"
Бэкап директорий данных
Код: BASH
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
#!/bin/bash
#
# Yandex.Disk backup script v1.0 by Sergey Lukonin (neblog.info) // modified by Anton Koudinov (itwrks.org)
#
#

# # # # # # # # # # НАСТРОЙКИ TAR # # # # # # # # # # #

# Параметры tar
TAR_PARAMS='-cvf'

# # # # # # # # # # ОБЩИЕ НАСТРОЙКИ # # # # # # # # # #

# Директория для временного хранения бэкапов, которые удаляются после отправки на Яндекс.Диск
BACKUP_DIR='/mnt/localfs/nfs/share/_backup/upload/WWW_DATA'

# Название проекта, используется в логах и именах архивов
PROJECT='itwrks.org'

# Сколько дней хранятся бэкапы на Яндекс.Диске (0 - хранить все бэкапы). Учитывайте расписание резервного копирования, так как дата для автоматического удаления рассчитывается от текущей минус количество дней, указанных в параметре.
# Например, делаете архивы каждый день и хотите хранить за 14 дней - задаёте 14. Делаете еженедельно и хотите хранить 4 архива - задаёте 30
MAX_BACKUPS='3'

# Директории для архивации (указываются через пробел), которые будут отправлены на Яндекс.Диск. Необходимо задать префикс для названия архива. Формат: <prefix:/path/to/dir>
DIR1='NOSQLRU:/mnt/localfs/nfs/share/_backup/WWW_DATA/nosql.ru/forum/user_data'
DIR2='RESQLRU:/mnt/localfs/nfs/share/_backup/WWW_DATA/resql.ru/forum/user_data'
DIR3='RED_FORUM:/mnt/localfs/nfs/share/_backup/WWW_DATA/red-forum.ru/forum/user_data'
DIRS="$DIR1 $DIR2 $DIR3"

# Yandex.Disk токен (как получить - см. на neblog.info)
TOKEN='123qweasdzxc'

# Директория приложения на Яндекс.Диске, куда будут отправлены архивы. Необходимо создать предварительно
APP_DIR='WWW_DATA'

# Произвольное имя расширения (с точкой!) для архива (Яндекс ограничивает скорость загрузки для некоторых MIME https://yadisk.readthedocs.io/ru/dev/known_issues.html. При необходимости восстановления расширение достаточно удалить и распаковать как обычный архив)
ext='.yarch'

# Имя лог-файла, хранится в директории, указанной в $BACKUP_DIR
LOGFILE='backup.log'

# E-mail для отправки результата выполнения скрипта. Оставьте пустым, если отправлять результаты не требуется
sendLog='root@itwrks.org'

# Отправлять только ошибки (true). Укажите false, если нужно отправлять логи при любом результате выполнения скрипта
sendLogErrorsOnly='false'

# Выбор сжатия. xz - сильнее сжатие, меленнее скорость. gzip - слабее сжатие, быстрее скорость. Оптимальный вариант - gzip
arch='gzip'

# # # # # # # # # # КОНЕЦ НАСТРОЕК # # # # # # # # # # # # #
# # # # # # # # ДАЛЬШЕ НИЧЕГО НЕ МЕНЯЕМ! # # # # # # # # # #

# Дата, используется в именах архивов для расчёта количества бэкапов для автоматического удаления
DATE="`date '+%Y%m%d'`.`date '+%H%M%S'`"

# # # # # # # # ДАЛЬШЕ НИЧЕГО НЕ МЕНЯЕМ! # # # # # # # # # #

cd $BACKUP_DIR || exit 88

function mailing()
{
    if [ ! $sendLog = '' ];then
        if [ "$sendLogErrorsOnly" == true ];
        then
            if echo "$1" | grep -q 'error'
            then
                echo "$2" | mail -s "$1" $sendLog > /dev/null
            fi
        else
            echo "$2" | mail -s "$1" $sendLog > /dev/null
        fi
    fi
}

function logger()
{
    echo "["`date "+%Y-%m-%d %H:%M:%S"`"] File $BACKUP_DIR: $1" >> $BACKUP_DIR/$LOGFILE
}

function parseJson()
{
    local output
    regex="(\"$1\":[\"]?)([^\",\}]+)([\"]?)"
    [[ $2 =~ $regex ]] && output=${BASH_REMATCH[2]}
    echo $output
}

function checkError()
{
    echo $(parseJson 'error' "$1")
}

function getUploadUrl()
{
    json_out=`curl -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources/upload/?path=app:/$APP_DIR/$backupName&overwrite=true"`
    json_error=$(checkError "$json_out")
    if [[ $json_error != '' ]];
    then
        logger "$PROJECT - Yandex.Disk error: $json_error"
        mailing "$PROJECT - Yandex.Disk backup error" "ERROR copy file $FILENAME. Yandex.Disk error: $json_error"
    echo ''
    else
        output=$(parseJson 'href' $json_out)
        echo $output
    fi
}

function uploadFile
{
    local json_out
    local uploadUrl
    local json_error
    uploadUrl=$(getUploadUrl)
    if [[ $uploadUrl != '' ]];
    then
    echo $UploadUrl
        json_out=`curl -s -T $1 -H "Authorization: OAuth $TOKEN" $uploadUrl`
        json_error=$(checkError "$json_out")
    if [[ $json_error != '' ]];
    then
        logger "$PROJECT - Yandex.Disk error: $json_error"
        mailing "$PROJECT - Yandex.Disk backup error" "ERROR copy file $FILENAME. Yandex.Disk error: $json_error"

    else
        logger "$PROJECT - Copying file to Yandex.Disk success"
        mailing "$PROJECT - Yandex.Disk backup success" "SUCCESS copy file $FILENAME"

    fi
    else
        echo 'Some errors occured. Check log file for detail'
    fi
}

function backups_list() {
    # Ищем в директории приложения все файлы бэкапов и выводим их названия:
    curl -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$APP_DIR/&sort=created&limit=100" | tr "{},[]" "\n" | grep "name[[:graph:]]*.tar.*$ext" | cut -d: -f 2 | tr -d '"' | grep -v ^http
}

function backups_count() {
    local bkps=$(backups_list | wc -l)
    echo $bkps
}

function remove_old_backups() {
    logger "Удаляем старые бэкапы с Яндекс.Диска"
    # Цикл удаления старых бэкапов:
    # - От текущей даты отсчитываем количество дней, указанных в параметре MAX_BACKUPS и определяем дату для удаления
    # - Основываясь на имени файла, удаляем бэкапы до необходимой даты
    bkps=$(backups_count)
    if [ $bkps -gt 0 ]; then
        old_bkps=$(date '+%Y%m%d' -d "-$MAX_BACKUPS day")
        while read entry; do
            old_entry=$(echo $entry | cut -f1 -d.)
            if [ $old_entry -lt $old_bkps ]; then
                logger "Удаляем $entry"
                curl -X DELETE -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$APP_DIR/$(backups_list | awk '(NR == 1)')&permanently=true"
            fi
        done <<< `backups_list`
    fi
}

logger "--- $PROJECT START BACKUP $DATE ---"

logger "Делаем архивы директорий"

# Определяем расширение файла в зависомости от выбора архиватора
[ $arch = 'gzip' ] && ext_arch='gz'
[ $arch = 'xz' ] && ext_arch='xz'

for DIR in $DIRS
    do
        tar_name=$(echo $DIR | cut -f1 -d:)
        DIR=$(echo $DIR | cut -f2 -d:)
        logger "Делаем архив директории $DIR"
        tar $TAR_PARAMS - $DIR | $arch -c > $BACKUP_DIR/$DATE.$tar_name.tar.$ext_arch$ext
    done

for file in `ls $BACKUP_DIR --hide=$LOGFILE`
    do
        FILENAME=$file
        logger "Выгружаем на Яндекс.Диск архив $file"
        backupName=$file
        uploadFile $file
    done

logger "Удаляем архивы с диска"
find $BACKUP_DIR -maxdepth 1 -type f -name "*.tar.*$ext" -exec rm '{}' \;

# Удаляем старые бэкапы с Яндекс.Диска (если MAX_BACKUPS > 0)
if [ $MAX_BACKUPS -gt 0 ]; then remove_old_backups; fi

logger "Завершение скрипта бэкапа"
Точка вызова для cron
Код: BASH
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
#!/bin/bash
#

sname="srv-adm.srv.itwrks"
scdir="/srv/scripts/backup"
sc_backup_mysql="$scdir/backup_mysql.sh"
#sc_backup_sys="$scdir/backup_sys.sh"
sc_backup_www_app="$scdir/backup_www_app.sh"
sc_backup_www_data="$scdir/backup_www_data.sh"
sc_rsync_www_data="$scdir/rsync_www_data.sh"
email=root
RETVAL=0

do_backup_mysql() {
    $sc_backup_mysql
    [ $? -eq 0 ] || exit 88
}

do_backup_sys() {
    echo "sys"
    #$sc_backup_sys
    [ $? -eq 0 ] || exit 88
}

do_backup_www_app() {
    $sc_backup_www_app
    [ $? -eq 0 ] || exit 88
}

do_rsync_www_data() {
    $sc_rsync_www_data
    [ $? -eq 0 ] || exit 88
}

do_backup_www_data() {
    do_rsync_www_data
    $sc_backup_www_data
    [ $? -eq 0 ] || exit 88
}

do_backup_all() {
    do_backup_mysql
    sleep 5
    do_backup_www_data
    sleep 5
    do_backup_www_app
    #sleep 5
    #do_backup_sys
}

#####

case "$1" in
    --backup-all)
        do_backup_all
        ;;
    --backup-mysql)
        do_backup_mysql
        ;;
    --backup-sys)
        do_backup_sys
        ;;
    --backup-www-app)
        do_backup_www_app
        ;;
    --backup-www-data)
        do_backup_www_data
        ;;
    *)
        echo "Usage: $0 --backup-all | --backup-mysql | --backup-sys | --backup-www-app | --backup-www-data"
esac
На Яндекс Диске диске в браузерной версии выглядит так:
...
Изменено: 20.10.2025, 13:15 - basename
Рейтинг: 0 / 0
basename:
Тема опубликована.
20.10.2025, 13:10
    #34
basename
Модератор форума
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Скрипты: Бэкапы на Яндекс Диск через REST API
Пример скрипта синхронизации (специфично для данного окружения, данные вложений синхронизируются с веб-сервера на административный сервер, где выполняется архивирование и выгрузка)
Код: BASH
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
#!/bin/bash
#

MOUNTPOINT="/mnt/nfs/itwrks/srv/apache1/data-www"

mountpoint $MOUNTPOINT
[ $? -eq 0 ] || exit 88

rsync -avzh --ignore-existing /mnt/nfs/itwrks/srv/apache1/data-www/resql.ru/forum/user_data/ /mnt/localfs/nfs/share/_backup/WWW_DATA/resql.ru/forum/user_data/
rsync -avzh --ignore-existing /mnt/nfs/itwrks/srv/apache1/data-www/nosql.ru/forum/user_data/ /mnt/localfs/nfs/share/_backup/WWW_DATA/nosql.ru/forum/user_data/
rsync -avzh --ignore-existing /mnt/nfs/itwrks/srv/apache1/data-www/red-forum.ru/forum/user_data/ /mnt/localfs/nfs/share/_backup/WWW_DATA/red-forum.ru/forum/user_data/
...
Рейтинг: 0 / 0
20.10.2025, 13:14
    #35
basename
Модератор форума
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Скрипты: Бэкапы на Яндекс Диск через REST API
Пример скрипта бэкапа (дамп) БД системы мониторинга Zabbix PostgreSQL
Код: BASH
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
#!/bin/bash
#
# Yandex.Disk backup script v1.0 by Sergey Lukonin (neblog.info) // modified by Anton Koudinov (itwrks.org)
#
#

# # # # # # # # # # НАСТРОЙКИ БЭКАПА POSTGRESQL # # # # # # # # # #

#
# В домашней директории пользователя, из под которого будет запускаться программа pg_dump, создайте конфигурационный файл ~/.pgpass с правами доступа к нему 0600 и параметрами доступа к серверу POSTGRESQL следующего содержания:
#
## имя_узла:порт:база_данных:имя_пользователя:пароль
#

# Параметры pg_dump
PGDUMP_PARAMS='-h 10.39.39.51 -U backup -w'

# # # # # # # # # # ОБЩИЕ НАСТРОЙКИ # # # # # # # # # #

# Директория для временного хранения бэкапов, которые удаляются после отправки на Яндекс.Диск
BACKUP_DIR='/mnt/localfs/nfs/share/_backup/upload/DB_ZBX'

# Название проекта, используется в логах и именах архивов
PROJECT='itwrks.org'

# Сколько дней хранятся бэкапы на Яндекс.Диске (0 - хранить все бэкапы). Учитывайте расписание резервного копирования, так как дата для автоматического удаления рассчитывается от текущей минус количество дней, указанных в параметре.
# Например, делаете архивы каждый день и хотите хранить за 14 дней - задаёте 14. Делаете еженедельно и хотите хранить 4 архива - задаёте 30
MAX_BACKUPS='7'

# Базы данных для архивации (указываются через пробел), которые будут отправлены на Яндекс.Диск
DBS='zabbix'

# Yandex.Disk токен (как получить - см. на neblog.info)
TOKEN='123qweasdzxc'

# Директория приложения на Яндекс.Диске, куда будут отправлены архивы. Необходимо создать предварительно
APP_DIR='DB_ZBX'

# Произвольное имя расширения (с точкой!) для архива (Яндекс ограничивает скорость загрузки для некоторых MIME https://yadisk.readthedocs.io/ru/dev/known_issues.html. При необходимости восстановления расширение достаточно удалить и распаковать как обычный архив)
ext='.yarch'

# Имя лог-файла, хранится в директории, указанной в $BACKUP_DIR
LOGFILE='backup.log'

# E-mail для отправки результата выполнения скрипта. Оставьте пустым, если отправлять результаты не требуется
sendLog='root@itwrks.org'

# Отправлять только ошибки (true). Укажите false, если нужно отправлять логи при любом результате выполнения скрипта
sendLogErrorsOnly='false'

# Выбор сжатия. xz - сильнее сжатие, меленнее скорость. gzip - слабее сжатие, быстрее скорость. Оптимальный вариант - gzip
arch='gzip'

# # # # # # # # # # КОНЕЦ НАСТРОЕК # # # # # # # # # # # # #
# # # # # # # # ДАЛЬШЕ НИЧЕГО НЕ МЕНЯЕМ! # # # # # # # # # #

# Дата, используется в именах архивов для расчёта количества бэкапов для автоматического удаления
DATE="`date '+%Y%m%d'`.`date '+%H%M%S'`"

# # # # # # # # ДАЛЬШЕ НИЧЕГО НЕ МЕНЯЕМ! # # # # # # # # # #

cd $BACKUP_DIR || exit 88

function mailing()
{
    if [ ! $sendLog = '' ];then
        if [ "$sendLogErrorsOnly" == true ];
        then
            if echo "$1" | grep -q 'error'
            then
                echo "$2" | mail -s "$1" $sendLog > /dev/null
            fi
        else
            echo "$2" | mail -s "$1" $sendLog > /dev/null
        fi
    fi
}

function logger()
{
    echo "["`date "+%Y-%m-%d %H:%M:%S"`"] File $BACKUP_DIR: $1" >> $BACKUP_DIR/$LOGFILE
}

function parseJson()
{
    local output
    regex="(\"$1\":[\"]?)([^\",\}]+)([\"]?)"
    [[ $2 =~ $regex ]] && output=${BASH_REMATCH[2]}
    echo $output
}

function checkError()
{
    echo $(parseJson 'error' "$1")
}

function getUploadUrl()
{
    json_out=`curl -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources/upload/?path=app:/$APP_DIR/$backupName&overwrite=true"`
    json_error=$(checkError "$json_out")
    if [[ $json_error != '' ]];
    then
        logger "$PROJECT - Yandex.Disk error: $json_error"
        mailing "$PROJECT - Yandex.Disk backup error" "ERROR copy file $FILENAME. Yandex.Disk error: $json_error"
    echo ''
    else
        output=$(parseJson 'href' $json_out)
        echo $output
    fi
}

function uploadFile
{
    local json_out
    local uploadUrl
    local json_error
    uploadUrl=$(getUploadUrl)
    if [[ $uploadUrl != '' ]];
    then
    echo $UploadUrl
        json_out=`curl -s -T $1 -H "Authorization: OAuth $TOKEN" $uploadUrl`
        json_error=$(checkError "$json_out")
    if [[ $json_error != '' ]];
    then
        logger "$PROJECT - Yandex.Disk error: $json_error"
        mailing "$PROJECT - Yandex.Disk backup error" "ERROR copy file $FILENAME. Yandex.Disk error: $json_error"

    else
        logger "$PROJECT - Copying file to Yandex.Disk success"
        mailing "$PROJECT - Yandex.Disk backup success" "SUCCESS copy file $FILENAME"

    fi
    else
        echo 'Some errors occured. Check log file for detail'
    fi
}

function backups_list() {
    # Ищем в директории приложения все файлы бэкапов и выводим их названия:
    curl -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$APP_DIR/&sort=created&limit=100" | tr "{},[]" "\n" | grep "name[[:graph:]]*.sql.*$ext" | cut -d: -f 2 | tr -d '"' | grep -v ^http
}

function backups_count() {
    local bkps=$(backups_list | wc -l)
    echo $bkps
}

function remove_old_backups() {
    logger "Удаляем старые бэкапы с Яндекс.Диска"
    # Цикл удаления старых бэкапов:
    # - От текущей даты отсчитываем количество дней, указанных в параметре MAX_BACKUPS и определяем дату для удаления
    # - Основываясь на имени файла, удаляем бэкапы до необходимой даты
    bkps=$(backups_count)
    if [ $bkps -gt 0 ]; then
        old_bkps=$(date '+%Y%m%d' -d "-$MAX_BACKUPS day")
        while read entry; do
            old_entry=$(echo $entry | cut -f1 -d.)
            if [ $old_entry -lt $old_bkps ]; then
                logger "Удаляем $entry"
                curl -X DELETE -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$APP_DIR/$(backups_list | awk '(NR == 1)')&permanently=true"
            fi
        done <<< `backups_list`
    fi
}

logger "--- $PROJECT START BACKUP $DATE ---"

logger "Делаем дамп баз данных"

# Определяем расширение файла в зависомости от выбора архиватора
[ $arch = 'gzip' ] && ext_arch='gz'
[ $arch = 'xz' ] && ext_arch='xz'

for DB in $DBS
    do
        logger "Делаем дампы базы данных $DB"
        pg_dump $PGDUMP_PARAMS $DB | $arch -c > $BACKUP_DIR/$DATE.$DB.sql.$ext_arch$ext
    done

for file in `ls $BACKUP_DIR --hide=$LOGFILE`
    do
        FILENAME=$file
        logger "Выгружаем на Яндекс.Диск архив $file"
        backupName=$file
        uploadFile $file
    done

logger "Удаляем архивы с диска"
find $BACKUP_DIR -maxdepth 1 -type f -name "*.sql.*$ext" -exec rm '{}' \;

# Удаляем старые бэкапы с Яндекс.Диска (если MAX_BACKUPS > 0)
if [ $MAX_BACKUPS -gt 0 ]; then remove_old_backups; fi

logger "Завершение скрипта бэкапа"
...
Изменено: 20.10.2025, 13:16 - basename
Рейтинг: 0 / 0
20.10.2025, 13:20
    #36
basename
Модератор форума
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Скрипты: Бэкапы на Яндекс Диск через REST API
Пример бэкапа системных конфигураций
Код: BASH
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
#!/bin/bash
#
# Yandex.Disk backup script v1.0 by Sergey Lukonin (neblog.info) // modified by Anton Koudinov (itwrks.org)
#
#

# # # # # # # # # # НАСТРОЙКИ TAR # # # # # # # # # # #

# Параметры tar
TAR_PARAMS='-cvf'

# # # # # # # # # # ОБЩИЕ НАСТРОЙКИ # # # # # # # # # #

# Директория для временного хранения бэкапов, которые удаляются после отправки на Яндекс.Диск
BACKUP_DIR='/mnt/localfs/nfs/share/_backup/upload/SYS'

# Название проекта, используется в логах и именах архивов
PROJECT='itwrks.org'

# Сколько дней хранятся бэкапы на Яндекс.Диске (0 - хранить все бэкапы). Учитывайте расписание резервного копирования, так как дата для автоматического удаления рассчитывается от текущей минус количество дней, указанных в параметре.
# Например, делаете архивы каждый день и хотите хранить за 14 дней - задаёте 14. Делаете еженедельно и хотите хранить 4 архива - задаёте 30
MAX_BACKUPS='14'

# Директории для архивации (указываются через пробел), которые будут отправлены на Яндекс.Диск. Необходимо задать префикс для названия архива. Формат: <prefix:/path/to/dir>
DIR1='srv-adm_etc:/etc'
DIR2='srv-adm_root:/root'
DIR3='srv-adm_var_spool_cron:/var/spool/cron'
DIR4='srv-adm_home_sa:/home/sa'
DIR5='srv-adm_srv_scripts:/srv/scripts'
DIR6='srv-adm_home_le:/home/le'
DIR7='srv-adm_srv_certs:/srv/certs'
DIR8='srv-adm_home_deda:/home/deda'
DIRS="$DIR1 $DIR2 $DIR3 $DIR4 $DIR5 $DIR6 $DIR7 $DIR8"

# Yandex.Disk токен (как получить - см. на neblog.info)
TOKEN='123qweasdzxc'

# Директория приложения на Яндекс.Диске, куда будут отправлены архивы. Необходимо создать предварительно
APP_DIR='VM/srv-adm'

# Произвольное имя расширения (с точкой!) для архива (Яндекс ограничивает скорость загрузки для некоторых MIME https://yadisk.readthedocs.io/ru/dev/known_issues.html. При необходимости восстановления расширение достаточно удалить и распаковать как обычный архив)
ext='.yarch'

# Имя лог-файла, хранится в директории, указанной в $BACKUP_DIR
LOGFILE='backup.log'

# E-mail для отправки результата выполнения скрипта. Оставьте пустым, если отправлять результаты не требуется
sendLog='root@itwrks.org'

# Отправлять только ошибки (true). Укажите false, если нужно отправлять логи при любом результате выполнения скрипта
sendLogErrorsOnly='false'

# Выбор сжатия. xz - сильнее сжатие, меленнее скорость. gzip - слабее сжатие, быстрее скорость. Оптимальный вариант - gzip
arch='gzip'

# # # # # # # # # # КОНЕЦ НАСТРОЕК # # # # # # # # # # # # #
# # # # # # # # ДАЛЬШЕ НИЧЕГО НЕ МЕНЯЕМ! # # # # # # # # # #

# Дата, используется в именах архивов для расчёта количества бэкапов для автоматического удаления
DATE="`date '+%Y%m%d'`.`date '+%H%M%S'`"

# # # # # # # # ДАЛЬШЕ НИЧЕГО НЕ МЕНЯЕМ! # # # # # # # # # #

cd $BACKUP_DIR || exit 88

function mailing()
{
    if [ ! $sendLog = '' ];then
        if [ "$sendLogErrorsOnly" == true ];
        then
            if echo "$1" | grep -q 'error'
            then
                echo "$2" | mail -s "$1" $sendLog > /dev/null
            fi
        else
            echo "$2" | mail -s "$1" $sendLog > /dev/null
        fi
    fi
}

function logger()
{
    echo "["`date "+%Y-%m-%d %H:%M:%S"`"] File $BACKUP_DIR: $1" >> $BACKUP_DIR/$LOGFILE
}

function parseJson()
{
    local output
    regex="(\"$1\":[\"]?)([^\",\}]+)([\"]?)"
    [[ $2 =~ $regex ]] && output=${BASH_REMATCH[2]}
    echo $output
}

function checkError()
{
    echo $(parseJson 'error' "$1")
}

function getUploadUrl()
{
    json_out=`curl -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources/upload/?path=app:/$APP_DIR/$backupName&overwrite=true"`
    json_error=$(checkError "$json_out")
    if [[ $json_error != '' ]];
    then
        logger "$PROJECT - Yandex.Disk error: $json_error"
        mailing "$PROJECT - Yandex.Disk backup error" "ERROR copy file $FILENAME. Yandex.Disk error: $json_error"
    echo ''
    else
        output=$(parseJson 'href' $json_out)
        echo $output
    fi
}

function uploadFile
{
    local json_out
    local uploadUrl
    local json_error
    uploadUrl=$(getUploadUrl)
    if [[ $uploadUrl != '' ]];
    then
    echo $UploadUrl
        json_out=`curl -s -T $1 -H "Authorization: OAuth $TOKEN" $uploadUrl`
        json_error=$(checkError "$json_out")
    if [[ $json_error != '' ]];
    then
        logger "$PROJECT - Yandex.Disk error: $json_error"
        mailing "$PROJECT - Yandex.Disk backup error" "ERROR copy file $FILENAME. Yandex.Disk error: $json_error"

    else
        logger "$PROJECT - Copying file to Yandex.Disk success"
        mailing "$PROJECT - Yandex.Disk backup success" "SUCCESS copy file $FILENAME"

    fi
    else
        echo 'Some errors occured. Check log file for detail'
    fi
}

function backups_list() {
    # Ищем в директории приложения все файлы бэкапов и выводим их названия:
    curl -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$APP_DIR/&sort=created&limit=100" | tr "{},[]" "\n" | grep "name[[:graph:]]*.tar.*$ext" | cut -d: -f 2 | tr -d '"' | grep -v ^http
}

function backups_count() {
    local bkps=$(backups_list | wc -l)
    echo $bkps
}

function remove_old_backups() {
    logger "Удаляем старые бэкапы с Яндекс.Диска"
    # Цикл удаления старых бэкапов:
    # - От текущей даты отсчитываем количество дней, указанных в параметре MAX_BACKUPS и определяем дату для удаления
    # - Основываясь на имени файла, удаляем бэкапы до необходимой даты
    bkps=$(backups_count)
    if [ $bkps -gt 0 ]; then
        old_bkps=$(date '+%Y%m%d' -d "-$MAX_BACKUPS day")
        while read entry; do
            old_entry=$(echo $entry | cut -f1 -d.)
            if [ $old_entry -lt $old_bkps ]; then
                logger "Удаляем $entry"
                curl -X DELETE -s -H "Authorization: OAuth $TOKEN" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$APP_DIR/$(backups_list | awk '(NR == 1)')&permanently=true"
            fi
        done <<< `backups_list`
    fi
}

logger "--- $PROJECT START BACKUP $DATE ---"

logger "Делаем архивы директорий"

# Определяем расширение файла в зависомости от выбора архиватора
[ $arch = 'gzip' ] && ext_arch='gz'
[ $arch = 'xz' ] && ext_arch='xz'

for DIR in $DIRS
    do
        tar_name=$(echo $DIR | cut -f1 -d:)
        DIR=$(echo $DIR | cut -f2 -d:)
        logger "Делаем архив директории $DIR"
        tar $TAR_PARAMS - $DIR | $arch -c > $BACKUP_DIR/$DATE.$tar_name.tar.$ext_arch$ext
    done

for file in `ls $BACKUP_DIR --hide=$LOGFILE`
    do
        FILENAME=$file
        logger "Выгружаем на Яндекс.Диск архив $file"
        backupName=$file
        uploadFile $file
    done

logger "Удаляем архивы с диска"
find $BACKUP_DIR -maxdepth 1 -type f -name "*.tar.*$ext" -exec rm '{}' \;

# Удаляем старые бэкапы с Яндекс.Диска (если MAX_BACKUPS > 0)
if [ $MAX_BACKUPS -gt 0 ]; then remove_old_backups; fi

logger "Завершение скрипта бэкапа"
...
Рейтинг: 0 / 0
20.10.2025, 13:26
    #37
basename
Модератор форума
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Скрипты: Бэкапы на Яндекс Диск через REST API
Примеры вызова по расписанию из cron
Код: BASH
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h  dom mon dow   command

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root

# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name  command to be executed

#####

# Backup
1 1 * * * /bin/sleep $((RANDOM\%180)); /srv/scripts/backup/backup_sys.sh > /dev/null 2>&1
1 2 * * * /srv/scripts/backup/do_backup.sh --backup-all > /dev/null 2>&1
1 0 * * * /srv/scripts/backup/backup_pgsql.sh > /dev/null 2>&1
...
Рейтинг: 0 / 0
20.10.2025, 14:15
    #38
basename
Модератор форума
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Скрипты: Бэкапы на Яндекс Диск через REST API
При выгрузке архивов в Яндекс Диск есть нюанс: более 50 ГБ выгрузить нельзя (во всяком случае так было, может сейчас что-то изменилось). Надо будет рассмотреть вариант с автоматизацией разбиения на части. Насколько понимаю в этом случае придётся использовать zip.
...
Рейтинг: 0 / 0
Форумы / Публикации участников [закрыт для гостей] / Скрипты: Бэкапы на Яндекс Диск через REST API / 7 сообщений из 7, страница 1 из 1
Найденые пользователи ...
Разблокировать пользователей ...
Читали форум (0):
Пользователи онлайн (0):
x
x
Закрыть


Просмотр
0 / 0
Close
Debug Console [Select Text]