forked from FajeSu/site-backup-script
-
Notifications
You must be signed in to change notification settings - Fork 0
/
backup_script.sh
executable file
·498 lines (402 loc) · 16.3 KB
/
backup_script.sh
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
#!/bin/bash
#
# Скрипт для архивации базы данных и файлов сайта
# с последующей отправкой на Яндекс.Диск
#
# Основан на аналогичном скрипте от Сергея Луконина
# http://neblog.info/skript-bekapa-na-yandeks-disk/
#
# Версия: 1.2.3
# Автор: Евгений Хованский <[email protected]>
# Copyright: (с) 2020 Digital Fresh
# Сайт: https://www.d-fresh.ru/
#
# Обязательные ключи командной строки:
# -project-name название проекта, используется в журналах событий
# в именах архивов
# -db-user пользователь базы данных (для режима db)
# -db-pass пароль пользователя базы данных (для режима db)
# -project-dirs директории для архивации, через запятую (для режима files)
#
# Необязательные ключи командной строки:
# -mode выбор объекта архивации (режим):
# - db (база данных)
# - files (локальные файлы)
# (по-умолчанию "db,files", т.е. и БД, и файлы)
# (если указан только один объект, то обязательные
# ключи для другого становятся необязательными)
# -db-host сервер базы данных
# (по-умолчанию - localhost)
# -db-name название базы данных
# (по-умолчанию используются данные из
# ключа -db-user)
# -max-backups максимальное количество бэкапов,
# хранимых на Яндекс.Диске
# (0 - хранить все бэкапы)
# (по-умолчанию - 12)
# ------------------------------------------------------------
# --- Константы ---
# Путь до скрипта
# Используется в путях до архивов и файлов журналов событий
declare -r script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Время запуска скрипта
# Используется в именах архивов
declare -r backup_time="$(date "+%Y%m%d-%H%M%S")"
# Имя временного файла журнала событий
declare -r log_tmp_file="$(basename -s .sh "${BASH_SOURCE[0]}")_tmp_log_${backup_time}_$(tr -dc 'a-z0-9' < /dev/urandom | head -c 8).txt"
# Массив ответов сервера при загрузке файла на Яндекс.Диск
declare -r -A upload_response_code_statuses=(
["413"]="Размер файла превышает 10 ГБ"
["500"]="Внутренняя ошибка сервера"
["503"]="Сервер временно недоступен"
["507"]="Недостаточно места"
)
# --- Стандартные значения переменных настроек ---
# Настройки должны храниться в файле "_settings.sh" рядом с файлом скрипта
ya_token=""
log_file=""
send_log_to=""
send_log_from=""
send_log_errors_only=true
# Загружаем настройки
. "$script_path/_settings.sh"
# ------
# Добавление даты в начало строки события
function getLoggerString() {
echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1"
}
# Запись события во временный файл журнала
function logger() {
echo -e "$(getLoggerString "$1")" >> "$script_path/$log_tmp_file"
if [ -n "$send_log_to" ] && [ "$2" = "error" ]; then
email_log_error=true
fi
}
# Подготовка переменных
# Обработка ключей командной строки
function prepareVars() {
if [ -z "$send_log_from" ]; then
send_log_from="$send_log_to"
fi
if [ -z "$send_log_errors_only" ]; then
send_log_errors_only=false
fi
if [ -z "$ya_token" ]; then
logger "Ошибка! Не задано значение переменной \"ya_token\" в настройках" "error"
return 1
fi
while [ -n "$1" ]; do
case "$1" in
-project-name)
project_name="$2"
;;
-mode)
mode="$2"
;;
-db-user)
mysql_user="$2"
;;
-db-pass)
mysql_pass="$2"
;;
-project-dirs)
backup_dirs="${2//\~/$HOME}"
backup_dirs="${backup_dirs//,/ }"
;;
-db-host)
mysql_server="$2"
;;
-db-name)
mysql_db="$2"
;;
-max-backups)
max_backups="$2"
;;
esac
shift # past argument
shift # past value
done
local -A vars=(
["-project-name"]="$project_name"
)
local mode_tmp="$mode"
mode=""
if [[ $mode_tmp =~ "db" ]]; then
mode="db"
fi
if [[ $mode_tmp =~ "files" ]]; then
if [ -n "$mode" ]; then
mode="${mode},"
fi
mode="${mode}files"
fi
if [[ -z $mode ]]; then
mode="db,files"
fi
if [[ $mode =~ "db" ]]; then
vars["-db-user"]="$mysql_user"
vars["-db-pass"]="$mysql_pass"
fi
if [[ $mode =~ "files" ]]; then
vars["-project-dirs"]="$backup_dirs"
fi
local key
local error
for key in "${!vars[@]}"; do
if [ -z "${vars[$key]}" ]; then
error="Ошибка! Не указан ключ командной строки: $key"
if [ -n "$project_name" ]; then
error="$project_name - $error"
fi
logger "$error" "error"
logger "Обязательные ключи командной строки: ${!vars[*]}"
return 1
fi
done
if [ -z "$mysql_server" ]; then
mysql_server="localhost"
fi
if [ -z "$mysql_db" ]; then
mysql_db="$mysql_user"
fi
if [ -z "$max_backups" ]; then
max_backups="12"
fi
return 0
}
# Создание архивов базы данных и файлов
function createLocalFiles() {
mkdir "$script_path/${project_name}_${backup_time}"
if [[ $mode =~ "db" ]]; then
logger "Создание архива базы данных: dump_mysql_${project_name}_${backup_time}.sql.gz"
local mysql_error="$(((mysqldump -h "$mysql_server" -u "$mysql_user" -p"$mysql_pass" "$mysql_db" | gzip -9c | pv -qL 1M | split -b 2GB -d --additional-suffix=.sql.gz - "$script_path/${project_name}_${backup_time}/dump_mysql_${project_name}_${backup_time}_") 2>&1) | grep -v "Using a password on the command line interface can be insecure")"
if [ -n "$mysql_error" ]; then
logger "$mysql_error" "error"
return 1
fi
fi
if [[ $mode =~ "files" ]]; then
logger "Создание архива каталогов: files_${project_name}_${backup_time}.tar.gz"
local files_error="$((tar -cP $backup_dirs | gzip -9c | pv -qL 1M | split -b 2GB -d --additional-suffix=.tar.gz - "$script_path/${project_name}_${backup_time}/files_${project_name}_${backup_time}_") 2>&1)"
if [ -n "$files_error" ]; then
logger "$files_error" "error"
return 1
fi
fi
return 0
}
# Получение значения по ключу из данных json
# Использование: getByKeyFromJson "key" "json"
function getByKeyFromJson() {
local regex="\"$1\":\"?([^\",\}]+)\"?"
local output
[[ $2 =~ $regex ]] && output="${BASH_REMATCH[1]}"
echo "$output"
}
# Проверка наличия ошибки в ответе Яндекса
function checkError() {
echo "$(getByKeyFromJson "error" "$1")"
}
# Получение понятного для восприятия размера файла
function getHumanReadableFileSize() {
local regex="^[0-9]+$"
local file_size="$(du -b $1 2>&1 | cut -f 1)"
if [[ $file_size =~ $regex ]]; then
local bc_error="$(bc --version 2>&1 1>/dev/null)"
if [ $file_size -lt 1024 ] || [ -n "$bc_error" ]; then
file_size="$file_size Б"
if [ -n "$bc_error" ]; then
logger "Внимание! Команда \"bc\" не найдена" "error"
fi
else
local -A sizes_names=(
[$[1024**4]]="ТиБ"
[$[1024**3]]="ГиБ"
[$[1024**2]]="МиБ"
[1024]="КиБ"
)
local size
for ((size=1024**4; size>=1024; size/=1024)); do
if [ $file_size -ge $size ]; then
file_size="$(bc <<< "scale=2;$file_size/$size" 2>/dev/null) ${sizes_names[$size]}"
break
fi
done
fi
echo "$file_size"
else
logger "$file_size" "error"
echo ""
fi
}
# Получение адреса для загрузки файла
function getUploadUrl() {
local json_out="$(curl -s -H "Authorization: OAuth $ya_token" -H "Accept: application/json" -H "Content-Type: application/json" "https://cloud-api.yandex.net:443/v1/disk/resources/upload/?path=app:/$1&overwrite=true")"
local json_error="$(checkError "$json_out")"
if [ -n "$json_error" ]; then
logger "Ошибка получения адреса для загрузки файла $1: $json_error" "error"
echo ""
else
echo "$(getByKeyFromJson "href" "$json_out")"
fi
}
# Загрузка одного файла
function uploadFile() {
local file_basename="$(basename "$1")"
local file_size="$(getHumanReadableFileSize "$1")"
if [ -n "$file_size" ]; then
file_size=" ($file_size)"
fi
logger "Загрузка файла ${file_basename}${file_size} на Яндекс.Диск"
local upload_url="$(getUploadUrl "$project_name/$backup_time/$file_basename")"
if [ -n "$upload_url" ]; then
local response_code="$(curl -s -T "$1" -H "Authorization: OAuth $ya_token" -H "Accept: application/json" -H "Content-Type: application/json" -o /dev/null -w "%{http_code}" "$upload_url")"
if [ -n "$response_code" ] && [ -n "${upload_response_code_statuses[$response_code]}" ]; then
logger "Ошибка загрузки файла $file_basename: $response_code - ${upload_response_code_statuses[$response_code]}" "error"
return 1
fi
fi
return 0
}
# Удаление локального файла
function removeLocalFile() {
logger "Удаление локального файла $(basename "$1")"
rm -f "$1"
}
# Загрузка архивов на Яндекс.Диск
function upload() {
local json_out
local json_error
# Создание директорий на Яндекс.Диске
local path=""
local value
for value in "$project_name" "$backup_time"; do
path="$path$value/"
json_out="$(curl -X PUT -s -H "Authorization: OAuth $ya_token" -H "Accept: application/json" -H "Content-Type: application/json" "https://cloud-api.yandex.net:443/v1/disk/resources/?path=app:/$path")"
json_error="$(checkError "$json_out")"
if [ -n "$json_error" ] && [ "$json_error" != "DiskPathPointsToExistentDirectoryError" ]; then
logger "Ошибка создания директории $path в каталоге приложения на Яндекс.Диске: $json_error" "error"
return 1
fi
done
# Загрузка архивов
local file
for file in $script_path/${project_name}_${backup_time}/*; do
if [ -f "$file" ]; then
uploadFile "$file"
if [ $? -ne 0 ]; then
return 2
fi
# Удаление архива после успешной загрузки
removeLocalFile "$file"
fi
done
return 0
}
# Получение списка директорий, вложенных в каталог проекта
# https://tech.yandex.ru/disk/api/reference/meta-docpage/
function yandexDirList() {
curl -s -H "Authorization: OAuth $ya_token" -H "Accept: application/json" -H "Content-Type: application/json" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$project_name&fields=_embedded.items.name&limit=999&sort=-created&offset=$max_backups" | tr "{},[]" "\n" | grep "name" | cut -d: -f 2 | tr -d "\""
}
# Удаление старых бэкапов на Яндекс.Диске
function removeCloudOldBackups() {
if [ "$max_backups" -gt 0 ]; then
local dirs=($(yandexDirList))
if [ "${#dirs[@]}" -gt 0 ]; then
logger "Удаление старых бэкапов на Яндекс.Диске"
local dir
for dir in "${dirs[@]}"; do
curl -X DELETE -s -H "Authorization: OAuth $ya_token" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$project_name/$dir&force_async=true&permanently=true" >/dev/null
done
fi
fi
}
# Удаление последнего бэкапа на Яндекс.Диске после ошибки загрузки
function removeCloudLastBackup() {
logger "Удаление последнего бэкапа на Яндекс.Диске, загруженного с ошибкой"
curl -X DELETE -s -H "Authorization: OAuth $ya_token" "https://cloud-api.yandex.net:443/v1/disk/resources?path=app:/$project_name/$backup_time&force_async=true&permanently=true" >/dev/null
}
# Отправка письма с результатом выполнения скрипта
function mailing() {
if [ -n "$send_log_to" ]; then
if [ "$send_log_errors_only" = false ] || ([ ! "$send_log_errors_only" = false ] && [ "$email_log_error" = true ]); then
if [[ "$(mail -V 2>/dev/null)" =~ "mailutils" ]]; then
local mail_error="$(mail -s "Site backup script log" -a "From: $send_log_from" -a "Content-type: text/plain; charset=utf-8" "$send_log_to" <<<"$(cat "$script_path/$log_tmp_file")
$(getLoggerString "$1")" 2>&1)"
else
local mail_error="$(mail -s "Site backup script log" -r "$send_log_from" -S content-type="text/plain; charset=utf-8" "$send_log_to" <<<"$(cat "$script_path/$log_tmp_file")
$(getLoggerString "$1")" 2>&1)"
fi
if [ -n "$mail_error" ]; then
logger "Ошибка отправки почты! $mail_error"
return 1
fi
fi
fi
}
# Удаление локальных архивов
function removeLocalFiles() {
logger "Удаление локальных архивов"
rm -fr "$script_path/${project_name}_${backup_time}"
}
# Запись событий в общий файл журнала и удаление временного
function writeLog() {
if [ -z "$log_file" ]; then
log_file="$(basename -s .sh "${BASH_SOURCE[0]}")_log.txt"
fi
cat "$script_path/$log_tmp_file" >> "$script_path/$log_file"
rm -f "$script_path/$log_tmp_file"
}
# -----
# Название проекта
# Используется в журнале событий и в именах архивов
declare project_name
# Объект архивации: база данных, локальные файлы
declare mode
# Сервер базы данных
declare mysql_server
# Пользователь базы данных
declare mysql_user
# Пароль пользователя базы данных
declare mysql_pass
# Имя базы данных
declare mysql_db
# Директории для архивации (указываются через пробел),
# которые будут помещены в единый архив и отправлены на Яндекс.Диск
declare backup_dirs
# Максимальное количество бэкапов, хранимых на Яндекс.Диске
declare max_backups
# Результат выполнения скрипта содержит ошибки
declare email_log_error=false
# -----
shopt -s nocasematch
logger "--- Начало выполнения скрипта ---"
prepareVars $*
if [ $? -eq 0 ]; then
logger "Проект: $project_name"
createLocalFiles
if [ $? -eq 0 ]; then
upload
case $? in
# Ошибок нет
# Удаляем старые бэкапы
0)
removeCloudOldBackups
;;
# Ошибка создания директории на Яндекс.Диске
# Ничего не делаем
1)
;;
# Ошибка загрузки файла на Яндекс.Диск
# Удаляем последний загруженный бэкап
2)
removeCloudLastBackup
;;
esac
fi
removeLocalFiles
fi
mailing "--- Завершение выполнения скрипта ---"
logger "--- Завершение выполнения скрипта ---\n"
writeLog
shopt -u nocasematch