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

Инструменты сайта


service:nextcloud

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

Предыдущая версия справа и слеваПредыдущая версия
Следующая версия
Предыдущая версия
service:nextcloud [02.02.2024 06:06] – [Настройка] viacheslavservice:nextcloud [14.03.2025 10:21] (текущий) – [High-performance backend (HPB)] viacheslav
Строка 1: Строка 1:
 +====== Nextcloud ======
  
 +По ходу инсталляции, будут настроены "pretty URLs" (ссылки без index.php в составе), кэш и настройка аутентификации через LDAP. Fail2ban (утилита против брутфорса) рассматривается [[Fail2ban|в отдельной статье]].
 +
 +===== Установка и настройка системы и необходимых компонентов =====
 +Во время установки Ubuntu Server, отметить для установки SSH Server и LAMP. Выбрать автоустановку обновлений безопасности.
 +<code bash>
 +# Войти в режим рута
 +sudo -i
 +# Задать статический IP:
 +nano /etc/network/interfaces
 +</code>
 +<code>
 +auto eth0
 +iface eth0 inet static
 +address 192.168.1.7
 +netmask 255.255.255.0
 +gateway 192.168.1.1
 +dns-nameservers 192.168.1.1
 +dns-search workgroup
 +</code>
 +
 +Перезагрузиться.
 +
 +Зайти в систему по SSH и обновить систему целиком:
 +<code bash>
 +sudo -i
 +apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y && apt-get autoremove
 +</code>
 +
 +Установить все необходимые компоненты и, в данном случае, [[https://docs.nextcloud.com/server/11/admin_manual/configuration_server/caching_configuration.html|механизм кэширования APCu + Redis]] и [[https://docs.nextcloud.com/server/12/admin_manual/configuration_server/theming.html#theming-of-icons|компоненты для автогенерации favicon]]:
 +<code bash>
 +apt-get install php-zip php-xml php-gd php-json php-curl php-mbstring php-bz2 php-intl php-mcrypt php-apcu redis-server php-redis php-imagick libmagickcore-6.q16-6-extra -y
 +# Обновление PHP на 8.2
 +apt install php8.2 php8.2-fpm php8.2-bcmath php8.2-bz2 php8.2-curl php8.2-gd php8.2-gmp php8.2-igbinary php8.2-imagick \
 +php8.2-intl php8.2-ldap php8.2-mbstring php8.2-mcrypt php8.2-mysql php8.2-redis php8.2-simplexml php8.2-smbclient php8.2-xml php8.2-xsl php8.2-zip
 +</code>
 +
 +Если нужен SMB client (для подключения внешних накопителей в Nextcloud), LDAP и Midnight Commander:
 +<code bash>
 +apt-get install smbclient php-ldap mc -y
 +</code>
 +
 +===== Клиент =====
 +https://nextcloud.com/install/#install-clients
 +<code powershell>
 +# WebDAV
 +https://bva.dyndns.info/cloud/remote.php/dav/files/<username>
 +</code>
 +===== Установка Nextcloud =====
 +<code bash>
 +# Скачать последнюю версию
 +wget https://download.nextcloud.com/server/releases/latest.tar.bz2
 +# Распаковать архив в корневую папку веб-сервера
 +tar xjf latest.tar.bz2 --strip=1 -C /var/www/html
 +# Удалить исходный архив (если нужно)
 +rm latest.tar.bz2
 +# Создать папку для пользовательских данных
 +mkdir /var/nextcloud-data
 +# Дать права владельца веб-серверу:
 +chown -R www-data:www-data /var/www/html /var/nextcloud-data
 +# Перезапустить Apache:
 +systemctl restart apache2
 +# Создать базу MySQL с именем "nextcloud":
 +mysql -u root -p -e "create database nextcloud";
 +</code> 
 +
 +Открыть браузер, зайти на веб-интерфейс (здесь: 192.168.1.7), задать логин и пароль админа, путь к папке с данными пользователей (здесь: /var/nextcloud-data) и имя БД (здесь: nextcloud). Либо [[https://docs.nextcloud.com/server/12/admin_manual/configuration_server/occ_command.html#command-line-installation-label|настроить из командной строки]]:\\
 +FIXME - уточнить, можно ли тут обойтись без паролей
 +<code bash>
 +sudo -u www-data php /var/www/html/occ maintenance:install --database "mysql" --data-dir "/var/nextcloud-data" --database-name "nextcloud" --database-user "root" --database-pass "password" --admin-user "admin" --admin-pass "password"
 +</code>
 +===== Настройка =====
 +<code bash>
 +# Убрать закрывающую строку из конфига и заменить строку overwrite.cli.url на нужную.
 +# В sed экранирование апострофа безумное - '"'"'
 +sed -i '
 +/);/d
 +/overwrite.cli.url/c \'"'"'overwrite.cli.url\'"'"' => \'"'"'https://192.168.1.7\'"'"',' /var/www/html/config/config.php
 +
 +# Настроить конфиг - "pretty URLs", кэширование, часовой пояс для логов и их ротацию (100 МБ)
 +echo "'htaccess.RewriteBase' => '/',
 +'memcache.local' => '\OC\Memcache\APCu',
 +'memcache.locking' => '\OC\Memcache\Redis',
 + 'redis' => array(
 +      'host' => 'localhost',
 +      'port' => 6379,
 +       ),
 +'logtimezone' => 'Europe/Moscow',
 +'log_rotate_size' => 104857600,
 +);" >> /var/www/html/config/config.php
 +
 +# Настроить максимальный размер файла на закачку в PHP и лимит памяти
 +# Проверить версию PHP и путь к используемым php.ini (php --ini), например, он может быть
 +# /etc/php/7.3/fpm/php.ini. Есть ещё
 +# /etc/php/7.3/cli/php.ini.
 +sed -i '
 +/upload_max_filesize =/c upload_max_filesize = 4G
 +/post_max_size =/c post_max_size = 4G
 +/memory_limit =/c memory_limit = 512M' /etc/php82/php.ini
 +
 +# Настроить параметры opcache
 +sed -i '
 +/opcache.enable=/c opcache.enable=1
 +/opcache.enable_cli=/c opcache.enable_cli=1
 +/opcache.memory_consumption=/c opcache.memory_consumption=128
 +/opcache.interned_strings_buffer=/c opcache.interned_strings_buffer=8
 +/opcache.max_accelerated_files=/c opcache.max_accelerated_files=10000
 +/opcache.revalidate_freq=/c opcache.revalidate_freq=1
 +/opcache.save_comments=/c opcache.save_comments=1
 +/opcache.interned_strings_buffer=/c opcache.interned_strings_buffer=16
 +' /etc/php82/php.ini
 +</code>
 +
 +[[http://php.net/manual/en/timezones.php|Список часовых поясов для PHP]]
 +==== SSL, mod_env и mod_rewrite для pretty URLs ====
 +<code bash>
 +a2enmod ssl headers env rewrite && a2ensite default-ssl
 +</code>
 +
 +Включить Strict transport security, [[https://www.w3.org/TR/referrer-policy/|Referrer Policy]] и Forward secrecy:
 +<code bash>
 +echo "<IfModule mod_headers.c>
 + Header always set Strict-Transport-Security "max-age=15768000; includeSubDomains"
 + Header always set Referrer-Policy "no-referrer-when-downgrade"
 +</IfModule>
 +
 +# Set Forward Secrecy
 +SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
 +SSLHonorCipherOrder on
 +SSLCipherSuite HIGH:!aNULL:!MD5:!3DES
 +" >> /etc/apache2/sites-available/default-ssl.conf
 +</code>
 +
 +==== Перенаправить HTTP на HTTPS ====
 +nano /etc/apache2/sites-available/000-default.conf
 +<code bash>
 +<VirtualHost *:80>
 +ServerName www.yourdomain.com
 +Redirect / https://www.yourdomain.com/
 +</VirtualHost>
 +</code>
 +<code bash>
 +systemctl restart apache2
 +</code>
 +
 +==== Настроить Pretty URLs ====
 +<code bash>
 +nano /etc/apache2/apache2.conf
 +
 +# В разделе <Directory /var/www> изменить параметр AllowOverride None на AllowOverride All
 +# Выйти из редактора.
 +
 +# Обновить файл .htaccess:
 +sudo -u www-data php /var/www/html/occ maintenance:update:htaccess
 +
 +# Перезапустить Apache:
 +systemctl restart apache2
 +</code>
 +
 +==== Переключить фоновые задачи на выполнение кроном ====
 +<code bash>
 +echo "# Nextcloud cron schedule" >> /etc/crontab
 +echo "*/15  *  *  *  * www-data php -f /var/www/html/cron.php" >> /etc/crontab
 +
 +sudo -u www-data php /var/www/html/occ background:cron
 +</code>
 +https://docs.nextcloud.com/server/13/admin_manual/configuration_server/background_jobs_configuration.html
 +
 +==== Выключить ненужные ссылки ====
 +config/config.php:
 +<code php>
 +# Выключить ссылку на сброс пароля
 +'lost_password_link' => 'disabled',
 +# Remove link “Get your own free account”
 +'simpleSignUpLink.shown' => false,
 +</code>
 +
 +==== Настроить аутентификацию через LDAP ====
 +
 +Для работы этого типа аутентификации необходимо установить компонент php-ldap, если он не был установлен ранее:
 +<code bash>
 +apt-get install php-ldap -y
 +</code>
 +
 +++++Далее настройка производится в веб-интерфейсе админа. |
 +
 +Прописать адрес контроллера домена, пользователя, который ходит за учётками в AD, и подразделение, из которого берутся пользователи. Здесь это корень домена, но можно указать, к примеру, корневое подразделение организации, чтобы ограничить область поиска.\\
 +{{:nextcloud-settings-ldap-server.png?400|}}
 +
 +Выборку критериев отбора объектов лучше всего прописывать вручную - в этом случае отключенные в AD пользователи исчезнут и из пользователей Nextcloud. Вот эта строка:
 +<code>
 +(&(&(objectclass=user)(objectcategory=person))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
 +</code>
 +{{:nextcloud-settings-ldap-users.png?400|}}
 +
 +Вкладка Advanced - Connection settings: прописать запасной контроллер домена.\\
 +{{:nextcloud-settings-ldap-adv-conn-set.png?400|}}
 +
 +Вкладка Advanced - Directory settings: указать, как показывать имена пользователей и групп в интерфейсе. По умолчанию пользователи показываются в виде их UUID, что совершенно неприемлемо. Также нужно прописать пути к корневому подразделению организации. Нижнему параметру выставить значение member (AD).\\
 +{{:nextcloud-settings-ldap-adv-dir-set.png?400|}}
 +
 +Вкладка Advanced - Special attributes: задаём, откуда брать почтовый адрес пользователя и критерий именования личного каталога пользователя на сервере. Почтовый адрес нужен, чтобы можно было слать уведомления о появлении общих ресурсов прямо из интерфейса, не копируя ссылку. Для этого необходимо заполнить поле почтового адреса в AD у всех пользователей. Личный каталог пользователя по умолчанию именуется значением UUID. Гораздо лучше, если папка будет называться соответственно логину.\\
 +{{:nextcloud-settings-ldap-adv-spec-attr.png?400|}}
 +
 +Вкладка Expert: задать внутреннее имя пользователя как логин в AD вместо неудобоваримого UUID.\\
 +{{:nextcloud-settings-ldap-expert.png?400|}}++++
 +
 +==== Онлайн-офис ====
 +<code powershell>
 +# Collabora (довольно тормозной вариант)
 +sudo -u www-data php /var/www/html/occ app:install richdocumentscode richdocuments
 +
 +# OnlyOffice (ограничение в 20 соединений в бесплатной версии)
 +sudo -u www-data php /var/www/html/occ app:install documentserver_community onlyoffice
 +</code>
 +
 +Для OnlyOffice, добавить в ''/var/www/html/config/config.php'':
 +<code php>
 +  'onlyoffice' =>
 +  array (
 +    'verify_peer_off' => true,
 +  ),
 +  'allow_local_remote_servers' => true,
 +</code>
 +
 +OnlyOffice - работа через прокси: https://helpcenter.onlyoffice.com/ru/installation/docs-community-proxy.aspx
 +
 +"Неизвестная ошибка" при открытии документа - нужно в конфиге Nextcloud
 +<code php>
 +'overwriteprotocol' => 'https', (add)
 +'overwrite.cli.url' => 'https' (change)
 +</code>
 +https://github.com/nextcloud/docker/issues/975
 +
 +==== Внешние хранилища ====
 +=== S3 ===
 +Способ доступа - Access key\\
 +Bucket - каталог в облаке, например, Media\\
 +Hostname - s3.cloud.mts.ru\\
 +Port - 443\\
 +Region - пусто\\
 +✔ Enable SSL\\
 +✔ Enable path style
 +=== SMB (CIFS) ===
 +Host - [IP address]\\
 +Share - Files (имя общего ресурса)\\
 +Remote subfolder - Отдел продаж\Группа впаривания\Обмен\\
 +Domain - example.com
 +
 +===== Обновление =====
 +<code bash>
 +# Автоматически:
 +sudo -u www-data php /var/www/html/updater/updater.phar
 +
 +# Вручную:
 +# Скачать последний релиз
 +wget https://download.nextcloud.com/server/releases/latest.tar.bz2
 +# Распаковать скачанный архив в папку установки
 +tar xjf latest.tar.bz2 --strip=1 -C /var/www/html
 +# Дать права владельца веб-серверу:
 +chown -R www-data:www-data /var/www/html
 +# Включить режим обслуживания
 +sudo -u www-data php /var/www/html/occ maintenance:mode --on
 +# Запустить процесс обновления
 +sudo -u www-data php /var/www/html/occ upgrade
 +# Выключить режим обслуживания
 +sudo -u www-data php /var/www/html/occ maintenance:mode --off
 +</code>
 +https://docs.nextcloud.com/server/latest/admin_manual/maintenance/update.html
 +
 +Экспресс-обновление со сменой шлюза
 +<code bash>
 +ip route change default via 192.168.1.254 dev eth0
 +apt update && apt upgrade -y && apt autoremove -y
 +sudo -u www-data php /var/www/html/updater/updater.phar --no-interaction
 +ip route change default via 192.168.1.1 dev eth0
 +</code>
 +
 +Обновление на след. мажорный релиз
 +<code bash>
 +# Нужно переключиться на бета-канал обновлений, обновляться, а затем переключиться обратно.
 +sudo -u www-data php /var/www/html/cloud/occ config:system:set updater.release.channel --value=beta
 +sudo -u www-data php /var/www/html/cloud/updater/updater.phar --no-interaction
 +sudo -u www-data php /var/www/html/cloud/occ config:system:set updater.release.channel --value=stable
 +</code>
 +
 +В Докере
 +<code bash>
 +docker exec nc-php sudo -u www-data php /var/www/html/cloud/occ config:system:set updater.release.channel --value=beta
 +docker exec nc-php sudo -u www-data php /var/www/html/cloud/updater/updater.phar --no-interaction
 +docker exec nc-php sudo -u www-data php /var/www/html/cloud/occ config:system:set updater.release.channel --value=stable
 +</code>
 +
 +==== Обновление всех приложений ====
 +<code bash>
 +sudo -u www-data php /var/www/html/cloud/occ app:update --all
 +# docker
 +docker exec -uwww-data nc-php php /var/www/html/cloud/occ app:update --all
 +</code>
 +
 +===== Настройка кэширования через сервер Redis =====
 +Как-то раз произошла ситуация - невозможно было стереть файл с сервера или обновить его, файл был заблокирован:
 +<WRAP round alert 60%>
 +file is locked\\
 +Error transferring bva.dyndns.info/cloud/remote.php/dav/files/user/123.txt - server replied: Locked ("123.txt" is locked)
 +</WRAP>
 +
 +[[https://help.nextcloud.com/t/file-is-locked-how-to-unlock/1883|В соответствующем howto]] советуют обнулить таблицу блокировок в базе mysql, а чтобы ситуация не повторялась, рекомендуют [[https://docs.nextcloud.com/server/12.0/admin_manual/configuration_server/caching_configuration.html#id3|поставить кэширующий сервис Redis]]. Так как у меня уже был APCu, было решено поставить Redis для блокировок, а APCu оставить для локального кэша. 
 +
 +В Ubuntu это ставится просто, а в Armbian в репозитории отсутствуют соответствующие пакеты, так что пришлось их собирать из исходников.
 +
 +==== Установить Redis ====
 +<WRAP round important 60%>
 +Информация устарела, в репозиториях для процессоров ARM появились собранные пакеты.\\
 +Теперь достаточно выполнить команду\\
 +apt-get install redis-server php-redis
 +</WRAP>
 +
 +++++ Сборка из исходников |
 +<code bash>
 +# Установить пакет tcl для возможности протестировать инсталлятор после сборки, возможно, он не нужен, т. к. тестировать необязательно
 +apt-get install tcl
 +# Скачать архив
 +wget http://download.redis.io/redis-stable.tar.gz
 +# Распаковать
 +tar xzf redis-stable.tar.gz
 +# Зайти в папку
 +cd redis-stable
 +# Собрать
 +make && make install
 +# Установить сервер
 +utils/install_server.sh
 +</code>
 +
 +После вопросов, которые задаются после установки, и где я соглашался с ответами по умолчанию, конфиг выглядит так:
 +<code>
 +Selected config:
 +Port           : 6379
 +Config file    : /etc/redis/6379.conf
 +Log file       : /var/log/redis_6379.log
 +Data dir       : /var/lib/redis/6379
 +Executable     : /usr/local/bin/redis-server
 +Cli Executable : /usr/local/bin/redis-cli
 +</code>
 +
 +https://redis.io/download\\
 +http://easy-code.ru/lesson/install-and-use-redis
 +
 +=== Установить плагин Redis для PHP ===
 +<code bash>
 +# Для сборки надо установить инструменты разработчика
 +apt-get install php7.0-dev
 +# Скачать
 +wget https://github.com/phpredis/phpredis/archive/3.1.2.tar.gz
 +# Распаковать
 +tar xzf 3.1.2.tar.gz
 +# Зайти в папку
 +cd phpredis-3.1.2
 +# Сделать пакет прилагаемым скриптом
 +.\mkdeb.sh
 +# Установить
 +make install
 +</code>
 +
 +Информация после установки:
 +<code>
 +PHP         : /usr/bin/php7.0
 +PHP_SAPI    : cli
 +PHP_VERSION : 7.0.18-0ubuntu0.16.04.1
 +ZEND_VERSION: 3.0.0
 +PHP_OS      : Linux - Linux orangepipcplus 3.4.113-sun8i #10 SMP PREEMPT Thu Feb 23 19:55:00 CET 2017 armv7l
 +INI actual  : /root/redis-3.2.9/utils/phpredis-3.1.2/tmp-php.ini
 +More .INIs  :
 +CWD         : /root/redis-3.2.9/utils/phpredis-3.1.2
 +Extra dirs  :
 +VALGRIND    : Not used
 +</code>
 +
 +Путь к плагину:
 +<code>
 +Installing shared extensions:     /usr/lib/php/20151012/
 +</code>
 +
 +https://habrahabr.ru/post/134974/\\
 +https://habrahabr.ru/sandbox/60933/
 +++++
 +==== Настройка Nextcloud и разблокировка файлов ====
 +Конфиг Nextcloud в части кэширования нужно привести к следующему виду:
 +<code php>
 +'memcache.local' => '\OC\Memcache\APCu',
 +'memcache.locking' => '\OC\Memcache\Redis',
 + 'redis' => array(
 +      'host' => 'localhost',
 +      'port' => 6379,
 +       ),
 +</code>
 +
 +<code bash>
 +# Перевести Nextcloud в режим обслуживания:
 +sudo -u www-data php /var/www/html/occ maintenance:mode --on
 +# Зайти в базу "cloud" и очистить блокировки:
 +mysql -u root -p cloud
 +</code>
 +<code mysql>
 +DELETE FROM oc_file_locks WHERE 1;
 +quit
 +</code>
 +
 +<code bash>
 +# Вывести Nextcloud из режима обслуживания:
 +sudo -u www-data php /var/www/html/occ maintenance:mode --off
 +# Перезапустить Apache:
 +systemctl restart apache2
 +</code>
 +
 +
 +===== Дополнительные материалы =====
 +==== Сертификаты ====
 +<code bash>
 +#Сделать папочку для сертификатов
 +mkdir /etc/ssl/certs/nextcloud
 +# самоподписанный сертификат на 10 лет без запроса пароля
 +openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/ssl/certs/nextcloud/nextcloud.key -out /etc/ssl/certs/nextcloud/nextcloud.crt
 +</code>
 +
 +<code bash>
 +nano /etc/apache2/sites-available/default-ssl.conf
 +</code>
 +<code>
 +SSLCertificateKeyFile /etc/ssl/certs/nextcloud/nextcloud.key
 +SSLCertificateFile /etc/ssl/certs/nextcloud/nextcloud.crt
 +#SSLCACertificateFile /etc/ssl/certs/nextcloud/nextcloud-int.crt
 +</code>
 +
 +FIXME
 +<code bash>
 +sed -i '
 +/SSLCertificateKeyFile/c SSLCertificateKeyFile /etc/ssl/certs/nextcloud/nextcloud.key
 +/SSLCertificateFile/c SSLCertificateFile /etc/ssl/certs/nextcloud/nextcloud.crt' /etc/apache2/sites-available/default-ssl.conf
 +</code>
 +
 +==== Импорт контактов из файла vcf ====
 +Проблема: выгруженный файл vcf с мобильника на Android 4.4 не загружается в приложение "Контакты" в Nextcloud.
 +
 +Решение:
 +  - Открыть файл в программе [[http://alexboiko.narod.ru/prod.html|tcode]] (в Windows), чтобы строки с кодировкой Quoted Printable перекодировались в нормальный русский текст. Это можно сделать и из командной строки: <code dos>tcode input.vcf /auto output.vcf</code>
 +  - Убрать из всего файла строки **;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE**
 +  - Строки **VERSION:2.1** заменить на **VERSION:3.0**
 +  - Сохранить файл в кодировке UTF-8.
 +==== Ссылка на выгруженные контакты мобильным приложением ====
 +%%https://path-to-nextcloud-site.com/apps/files/?dir=/.Contacts-Backup%%
 +
 +==== Полезные плагины ====
 +[[https://apps.nextcloud.com/apps/suspicious_login|Suspicious login detection]], [[https://apps.nextcloud.com/apps/audioplayer|Audio player]], [[https://apps.nextcloud.com/apps/onlyoffice|OnlyOffice]]
 +=== Плагин для Outlook ===
 +Версия 3: https://download.sendent.nl/addin/latest/Free/Free.zip
 +
 +++++ Устаревший (версии 2) |
 +https://download.nextcloud.com/outlook/
 +Ставить нужно плагин той же разрядности, что и у Офиса (не системы в целом).
 +
 +Cкачать свежую версию:
 +<code powershell>
 +# ((Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
 +# ? {$_.Displayname -match 'Microsoft Office' -and $_.InstallLocation -match "\w"} |
 +# select -expand installlocation -First 1) -split '\\')[1] -match 'x86'
 +
 +$offx64 = gci "$env:ProgramFiles\Microsoft Office\outlook.exe" -Recurse
 +$offx86 = gci "${env:ProgramFiles(x86)}\Microsoft Office\outlook.exe" -Recurse
 +$u = "https://download.nextcloud.com/outlook/"
 +$f = (curl $u).links |? href -like "20*" | select -Last 1 -expand href
 +$uf = $u + $f
 +if ($offx64) {
 +$plugx64 = (curl $uf).links |? href -like "*64*.msi" | select -expand href
 +$url64 = $uf + $plugx64
 +curl $url64 -OutFile "$env:userprofile\Downloads\$plugx64"
 +}
 +if ($offx86) {
 +$plugx86 = (curl $uf).links |? href -like "*86*.msi" | select -expand href
 +$url86 = $uf + $plugx86
 +curl $url86 -OutFile "$env:userprofile\Downloads\$plugx86"
 +}
 +</code>
 +
 +<code powershell>
 +# Путь к файлам перевода:
 +${env:ProgramFiles(x86)}\Nextcloud Outlook\Resources\Translations
 +</code>
 +
 +Перевод на русский:
 +<code json>
 +{
 +"Code":"ru-RU",
 +"Description":"Russian",
 +  "Translations": {
 +    "btApply": "Применить",
 +    "btDay": "1 день",
 +    "btMonth": "1 месяц",
 +    "btWeek": "1 неделя",
 +    "btDefault": "Стандартно",
 +    "lblExplExpDate": "Здесь можно установить дату истечения срока действия для общего ресурса.",
 +    "btPassword": "Задать пароль",
 +    "btExpire": "Срок действия",
 +    "btUploadFU": "Выгрузить файлы",
 +    "btFiles": "Выбрать файлы",
 +    "lblExplFU": "Здесь вы можете выгрузить файлы на сервер Nextcloud, которые слишком велики для почтовых вложений.",
 +    "lblExplPass": "Здесь вы можете задать пароль для общего ресурса. Стандартно пароль генерируется автоматически.",
 +    "btGenerate": "Генерировать",
 +    "btSetup": "Подключение",
 +    "btAdvanced": "Доп. настройки",
 +    "lblServerVal": "Подключено к",
 +    "lblServerInv": "Не подключено!",
 +    "btClose": "Закрыть",
 +    "lblUrl": "URL сервера Nextcloud",
 +    "lblUsername": "Логин",
 +    "lblPassword": "Пароль",
 +    "btConnect": "Подключиться",
 +    "btUploadSF": "Создать общий ресурс",
 +    "lblExplSF": "Здесь вы можете создать пространство на сервере Nextcloud для получения файлов от ваших адресатов, чтобы они могли выгружать файлы сразу на сервер без необходимости слать их по электронной почте.",
 +    "titleExpDate": "Срок действия",
 +    "titleFU": "Добавить файлы к письму",
 +    "titlePassword": "Задать пароль",
 +    "titleSettings": "Настройки",
 +    "titleAdvanced": "Дополнительно",
 +    "titleActivity": "Обзор действий",
 +    "titleAuthenticate": "Веб-вход",
 +    "titleSetShareName": "Задать имя общего ресурса",
 +    "titleSetup": "Подключение",
 +    "titleSF": "Создание общего ресурса",
 +    "btShareFiles": "Выгрузить файлы",
 +    "btSharePublicFolder": "Дать общий доступ к папке",
 +    "btActivity": "Обзор действий",
 +    "btSettings": "Настройки",
 +    "gpUpload": "Выгрузить",
 +    "gpSettings": "Настройки",
 +    "gpActivity": "Действия",
 +    "gpNextcloud": "Nextcloud",
 +    "expDatePast": "Дата истечения срока действия не может быть в прошлом.",
 +    "viewExtendPwPolTxt": "Включена защита паролем, сгенерировать его автоматически?",
 +    "viewExtendPwPolTit": "Пароль не задан!",
 +    "passwordReq": "Заданный пароль не отвечает требованиям безопасности. Отсутствуют: ",
 +    "setupIncorrect": "Настройки заданы неверно. Проверьте URL сервера, имя пользователя и пароль.",
 +    "mainFileUploadCancelTxt": "Вы уверены, что хотите остановить выгрузку файлов?",
 +    "mainFileUploadCancelTit": "Внимание!",
 +    "noPassword": "Без пароля",
 +    "noExpDate": "Без срока действия",
 +    "settingsAdvancedFU": "HTML для выгрузки файлов",
 +    "settingsAdvancedSF": "HTML для общего доступа к папке",
 +    "btFilesClear": "Пусто",
 +    "lblLanguage": "Язык:",
 +    "enabled": "Включено",
 +    "disabled": "Отключено",
 +    "lblOverridePP": "Включить защиту паролем:",
 +    "UploadingFiles": "Выгрузка файлов",
 +    "UploadingFilesCancel": "Отмена выгрузки, подождите...",
 +    "OutlookAttachmentMode": "Режим вложения Outlook:",
 +    "btCancel": "Отмена",
 +    "btShareName": "Задать имя общего ресурса",
 +    "lblShareName": "Здесь вы можете задать произвольное имя для общего ресурса. Стандартно имя создаётся автоматически.",
 +    "titleShareName": "Имя общего ресурса",
 +    "dialogShareNameTextInUse": "Указанное имя общего ресурса '{0}' уже используется. Создать имя автоматически?",
 +    "dialogShareNameTitle": "Имя общего ресурса",
 +    "dialogShareNameNoName": "Имя общего ресурса не задано. Хотите использовать автоматически созданное?",
 +    "passwordNeeds": "Требования",
 +    "passwordCharacters": "символов как минимум",
 +    "passwordNumbers": "цифр",
 +    "passwordSpecials": "спецсимволов",
 +    "passwordLetters": "букв"
 +  }}
 +</code>
 +
 +++++
 +==== Автоудаление файлов ====
 +  - Install [[https://apps.nextcloud.com/apps/files_automatedtagging|Automated Tagging]] and [[https://apps.nextcloud.com/apps/files_retention|Retention]] app.
 +  - Create a retention-parent tag and tag the /tmp directory with it.
 +  - Create tags like auto-delete-two-weeks and use those for the retention settings (Admin>Workflow>File retention)
 +  - Create an automation tagging rule which tags files with eg. auto-delete-two-weeks. As a condition choose that they must be tagged with retention-parent (files created inside the /tmp directory will have this retention-parent tag).
 +  - Test it by uploading a file to /tmp and check whether auto-delete-two-weeks has been attached to it.
 +https://www.maxi-muth.de/3236/allgemeines/auto-delete-files-in-a-directory-after-a-certain-time-in-nextcloud/
 +
 +В автотэге не работает выбор плагина Outlook - метки не назначаются. Если сделать по старинке через regex, то всё нормально (для старой версии плагина Outlook, для новой работает).
 +<code>
 +Request user agent - matches - ((Mozilla\/).*( \().*(\))( Nextcloud-Outlook v).*)
 +</code>
 +https://nextcloud.com/blog/controlling-file-retention-on-files-uploaded-with-the-outlook-add-in/
 +==== Снежок ====
 +https://nextcloud.com/blog/holiday-fun-with-jsloader/
 +
 +Установить плагин JSLoader, загрузить туда следующий код:
 +
 +++++ snow.js |
 +<file javascript snow.js>
 +/** @license
 + * DHTML Snowstorm! JavaScript-based snow for web pages
 + * Making it snow on the internets since 2003. You're welcome.
 + * -----------------------------------------------------------
 + * Version 1.44.20131208 (Previous rev: 1.44.20131125)
 + * Copyright (c) 2007, Scott Schiller. All rights reserved.
 + * Code provided under the BSD License
 + * http://schillmania.com/projects/snowstorm/license.txt
 + */
 +/*jslint nomen: true, plusplus: true, sloppy: true, vars: true, white: true */
 +/*global window, document, navigator, clearInterval, setInterval */
 +
 +var snowStorm = (function(window, document) {
 +
 +  // --- common properties ---
 +
 +  this.autoStart = true;          // Whether the snow should start automatically or not.
 +  this.excludeMobile = true;      // Snow is likely to be bad news for mobile phones' CPUs (and batteries.) Enable at your own risk.
 +  this.flakesMax = 96;           // Limit total amount of snow made (falling + sticking)
 +  this.flakesMaxActive = 48;      // Limit amount of snow falling at once (less = lower CPU use)
 +  this.animationInterval = 33;    // Theoretical "miliseconds per frame" measurement. 20 = fast + smooth, but high CPU use. 50 = more conservative, but slower
 +  this.useGPU = true;             // Enable transform-based hardware acceleration, reduce CPU load.
 +  this.className = null;          // CSS class name for further customization on snow elements
 +  this.excludeMobile = true;      // Snow is likely to be bad news for mobile phones' CPUs (and batteries.) By default, be nice.
 +  this.flakeBottom = null;        // Integer for Y axis snow limit, 0 or null for "full-screen" snow effect
 +  this.followMouse = true;        // Snow movement can respond to the user's mouse
 +  this.snowColor = '#B2B5C6';        // Don't eat (or use?) yellow snow.
 +  this.snowCharacter = '❄';  // &bull; = bullet, &middot; is square on some systems etc.
 +  this.snowStick = true;          // Whether or not snow should "stick" at the bottom. When off, will never collect.
 +  this.targetElement = null;      // element which snow will be appended to (null = document.body) - can be an element ID eg. 'myDiv', or a DOM node reference
 +  this.useMeltEffect = true;      // When recycling fallen snow (or rarely, when falling), have it "melt" and fade out if browser supports it
 +  this.useTwinkleEffect = true;  // Allow snow to randomly "flicker" in and out of view while falling
 +  this.usePositionFixed = false;  // true = snow does not shift vertically when scrolling. May increase CPU load, disabled by default - if enabled, used only where supported
 +  this.usePixelPosition = false;  // Whether to use pixel values for snow top/left vs. percentages. Auto-enabled if body is position:relative or targetElement is specified.
 +
 +  // --- less-used bits ---
 +
 +  this.freezeOnBlur = true;       // Only snow when the window is in focus (foreground.) Saves CPU.
 +  this.flakeLeftOffset = 0;       // Left margin/gutter space on edge of container (eg. browser window.) Bump up these values if seeing horizontal scrollbars.
 +  this.flakeRightOffset = 0;      // Right margin/gutter space on edge of container
 +  this.flakeWidth = 32;            // Max pixel width reserved for snow element
 +  this.flakeHeight = 32;           // Max pixel height reserved for snow element
 +  this.vMaxX = 5;                 // Maximum X velocity range for snow
 +  this.vMaxY = 4;                 // Maximum Y velocity range for snow
 +  this.zIndex = 5000;                // CSS stacking order applied to each snowflake
 +
 +  // --- "No user-serviceable parts inside" past this point, yadda yadda ---
 +
 +  var storm = this,
 +  features,
 +  // UA sniffing and backCompat rendering mode checks for fixed position, etc.
 +  isIE = navigator.userAgent.match(/msie/i),
 +  isIE6 = navigator.userAgent.match(/msie 6/i),
 +  isMobile = navigator.userAgent.match(/mobile|opera m(ob|in)/i),
 +  isBackCompatIE = (isIE && document.compatMode === 'BackCompat'),
 +  noFixed = (isBackCompatIE || isIE6),
 +  screenX = null, screenX2 = null, screenY = null, scrollY = null, docHeight = null, vRndX = null, vRndY = null,
 +  windOffset = 1,
 +  windMultiplier = 2,
 +  flakeTypes = 6,
 +  fixedForEverything = false,
 +  targetElementIsRelative = false,
 +  opacitySupported = (function(){
 +    try {
 +      document.createElement('div').style.opacity = '0.5';
 +    } catch(e) {
 +      return false;
 +    }
 +    return true;
 +  }()),
 +  didInit = false,
 +  docFrag = document.createDocumentFragment();
 +
 +  features = (function() {
 +
 +    var getAnimationFrame;
 +
 +    /**
 +     * hat tip: paul irish
 +     * http://paulirish.com/2011/requestanimationframe-for-smart-animating/
 +     * https://gist.github.com/838785
 +     */
 +
 +    function timeoutShim(callback) {
 +      window.setTimeout(callback, 1000/(storm.animationInterval || 20));
 +    }
 +
 +    var _animationFrame = (window.requestAnimationFrame ||
 +        window.webkitRequestAnimationFrame ||
 +        window.mozRequestAnimationFrame ||
 +        window.oRequestAnimationFrame ||
 +        window.msRequestAnimationFrame ||
 +        timeoutShim);
 +
 +    // apply to window, avoid "illegal invocation" errors in Chrome
 +    getAnimationFrame = _animationFrame ? function() {
 +      return _animationFrame.apply(window, arguments);
 +    } : null;
 +
 +    var testDiv;
 +
 +    testDiv = document.createElement('div');
 +
 +    function has(prop) {
 +
 +      // test for feature support
 +      var result = testDiv.style[prop];
 +      return (result !== undefined ? prop : null);
 +
 +    }
 +
 +    // note local scope.
 +    var localFeatures = {
 +
 +      transform: {
 +        ie:  has('-ms-transform'),
 +        moz: has('MozTransform'),
 +        opera: has('OTransform'),
 +        webkit: has('webkitTransform'),
 +        w3: has('transform'),
 +        prop: null // the normalized property value
 +      },
 +
 +      getAnimationFrame: getAnimationFrame
 +
 +    };
 +
 +    localFeatures.transform.prop = (
 +      localFeatures.transform.w3 || 
 +      localFeatures.transform.moz ||
 +      localFeatures.transform.webkit ||
 +      localFeatures.transform.ie ||
 +      localFeatures.transform.opera
 +    );
 +
 +    testDiv = null;
 +
 +    return localFeatures;
 +
 +  }());
 +
 +  this.timer = null;
 +  this.flakes = [];
 +  this.disabled = false;
 +  this.active = false;
 +  this.meltFrameCount = 20;
 +  this.meltFrames = [];
 +
 +  this.setXY = function(o, x, y) {
 +
 +    if (!o) {
 +      return false;
 +    }
 +
 +    if (storm.usePixelPosition || targetElementIsRelative) {
 +
 +      o.style.left = (x - storm.flakeWidth) + 'px';
 +      o.style.top = (y - storm.flakeHeight) + 'px';
 +
 +    } else if (noFixed) {
 +
 +      o.style.right = (100-(x/screenX*100)) + '%';
 +      // avoid creating vertical scrollbars
 +      o.style.top = (Math.min(y, docHeight-storm.flakeHeight)) + 'px';
 +
 +    } else {
 +
 +      if (!storm.flakeBottom) {
 +
 +        // if not using a fixed bottom coordinate...
 +        o.style.right = (100-(x/screenX*100)) + '%';
 +        o.style.bottom = (100-(y/screenY*100)) + '%';
 +
 +      } else {
 +
 +        // absolute top.
 +        o.style.right = (100-(x/screenX*100)) + '%';
 +        o.style.top = (Math.min(y, docHeight-storm.flakeHeight)) + 'px';
 +
 +      }
 +
 +    }
 +
 +  };
 +
 +  this.events = (function() {
 +
 +    var old = (!window.addEventListener && window.attachEvent), slice = Array.prototype.slice,
 +    evt = {
 +      add: (old?'attachEvent':'addEventListener'),
 +      remove: (old?'detachEvent':'removeEventListener')
 +    };
 +
 +    function getArgs(oArgs) {
 +      var args = slice.call(oArgs), len = args.length;
 +      if (old) {
 +        args[1] = 'on' + args[1]; // prefix
 +        if (len > 3) {
 +          args.pop(); // no capture
 +        }
 +      } else if (len === 3) {
 +        args.push(false);
 +      }
 +      return args;
 +    }
 +
 +    function apply(args, sType) {
 +      var element = args.shift(),
 +          method = [evt[sType]];
 +      if (old) {
 +        element[method](args[0], args[1]);
 +      } else {
 +        element[method].apply(element, args);
 +      }
 +    }
 +
 +    function addEvent() {
 +      apply(getArgs(arguments), 'add');
 +    }
 +
 +    function removeEvent() {
 +      apply(getArgs(arguments), 'remove');
 +    }
 +
 +    return {
 +      add: addEvent,
 +      remove: removeEvent
 +    };
 +
 +  }());
 +
 +  function rnd(n,min) {
 +    if (isNaN(min)) {
 +      min = 0;
 +    }
 +    return (Math.random()*n)+min;
 +  }
 +
 +  function plusMinus(n) {
 +    return (parseInt(rnd(2),10)===1?n*-1:n);
 +  }
 +
 +  this.randomizeWind = function() {
 +    var i;
 +    vRndX = plusMinus(rnd(storm.vMaxX,0.2));
 +    vRndY = rnd(storm.vMaxY,0.2);
 +    if (this.flakes) {
 +      for (i=0; i<this.flakes.length; i++) {
 +        if (this.flakes[i].active) {
 +          this.flakes[i].setVelocities();
 +        }
 +      }
 +    }
 +  };
 +
 +  this.scrollHandler = function() {
 +    var i;
 +    // "attach" snowflakes to bottom of window if no absolute bottom value was given
 +    scrollY = (storm.flakeBottom ? 0 : parseInt(window.scrollY || document.documentElement.scrollTop || (noFixed ? document.body.scrollTop : 0), 10));
 +    if (isNaN(scrollY)) {
 +      scrollY = 0; // Netscape 6 scroll fix
 +    }
 +    if (!fixedForEverything && !storm.flakeBottom && storm.flakes) {
 +      for (i=0; i<storm.flakes.length; i++) {
 +        if (storm.flakes[i].active === 0) {
 +          storm.flakes[i].stick();
 +        }
 +      }
 +    }
 +  };
 +
 +  this.resizeHandler = function() {
 +    if (window.innerWidth || window.innerHeight) {
 +      screenX = window.innerWidth - 16 - storm.flakeRightOffset;
 +      screenY = (storm.flakeBottom || window.innerHeight);
 +    } else {
 +      screenX = (document.documentElement.clientWidth || document.body.clientWidth || document.body.scrollWidth) - (!isIE ? 8 : 0) - storm.flakeRightOffset;
 +      screenY = storm.flakeBottom || document.documentElement.clientHeight || document.body.clientHeight || document.body.scrollHeight;
 +    }
 +    docHeight = document.body.offsetHeight;
 +    screenX2 = parseInt(screenX/2,10);
 +  };
 +
 +  this.resizeHandlerAlt = function() {
 +    screenX = storm.targetElement.offsetWidth - storm.flakeRightOffset;
 +    screenY = storm.flakeBottom || storm.targetElement.offsetHeight;
 +    screenX2 = parseInt(screenX/2,10);
 +    docHeight = document.body.offsetHeight;
 +  };
 +
 +  this.freeze = function() {
 +    // pause animation
 +    if (!storm.disabled) {
 +      storm.disabled = 1;
 +    } else {
 +      return false;
 +    }
 +    storm.timer = null;
 +  };
 +
 +  this.resume = function() {
 +    if (storm.disabled) {
 +       storm.disabled = 0;
 +    } else {
 +      return false;
 +    }
 +    storm.timerInit();
 +  };
 +
 +  this.toggleSnow = function() {
 +    if (!storm.flakes.length) {
 +      // first run
 +      storm.start();
 +    } else {
 +      storm.active = !storm.active;
 +      if (storm.active) {
 +        storm.show();
 +        storm.resume();
 +      } else {
 +        storm.stop();
 +        storm.freeze();
 +      }
 +    }
 +  };
 +
 +  this.stop = function() {
 +    var i;
 +    this.freeze();
 +    for (i=0; i<this.flakes.length; i++) {
 +      this.flakes[i].o.style.display = 'none';
 +    }
 +    storm.events.remove(window,'scroll',storm.scrollHandler);
 +    storm.events.remove(window,'resize',storm.resizeHandler);
 +    if (storm.freezeOnBlur) {
 +      if (isIE) {
 +        storm.events.remove(document,'focusout',storm.freeze);
 +        storm.events.remove(document,'focusin',storm.resume);
 +      } else {
 +        storm.events.remove(window,'blur',storm.freeze);
 +        storm.events.remove(window,'focus',storm.resume);
 +      }
 +    }
 +  };
 +
 +  this.show = function() {
 +    var i;
 +    for (i=0; i<this.flakes.length; i++) {
 +      this.flakes[i].o.style.display = 'block';
 +    }
 +  };
 +
 +  this.SnowFlake = function(type,x,y) {
 +    var s = this;
 +    this.type = type;
 +    this.x = x||parseInt(rnd(screenX-20),10);
 +    this.y = (!isNaN(y)?y:-rnd(screenY)-12);
 +    this.vX = null;
 +    this.vY = null;
 +    this.vAmpTypes = [1,1.2,1.4,1.6,1.8]; // "amplification" for vX/vY (based on flake size/type)
 +    this.vAmp = this.vAmpTypes[this.type] || 1;
 +    this.melting = false;
 +    this.meltFrameCount = storm.meltFrameCount;
 +    this.meltFrames = storm.meltFrames;
 +    this.meltFrame = 0;
 +    this.twinkleFrame = 0;
 +    this.active = 1;
 +    this.fontSize = (10+(this.type/5)*10);
 +    this.o = document.createElement('div');
 +    this.o.innerHTML = storm.snowCharacter;
 +    if (storm.className) {
 +      this.o.setAttribute('class', storm.className);
 +    }
 +    this.o.style.color = storm.snowColor;
 +    this.o.style.position = (fixedForEverything?'fixed':'absolute');
 +    if (storm.useGPU && features.transform.prop) {
 +      // GPU-accelerated snow.
 +      this.o.style[features.transform.prop] = 'translate3d(0px, 0px, 0px)';
 +    }
 +    this.o.style.width = storm.flakeWidth+'px';
 +    this.o.style.height = storm.flakeHeight+'px';
 +    this.o.style.fontFamily = 'arial,verdana';
 +    this.o.style.cursor = 'default';
 +    this.o.style.overflow = 'hidden';
 +    this.o.style.fontWeight = 'normal';
 +    this.o.style.zIndex = storm.zIndex;
 +    docFrag.appendChild(this.o);
 +
 +    this.refresh = function() {
 +      if (isNaN(s.x) || isNaN(s.y)) {
 +        // safety check
 +        return false;
 +      }
 +      storm.setXY(s.o, s.x, s.y);
 +    };
 +
 +    this.stick = function() {
 +      if (noFixed || (storm.targetElement !== document.documentElement && storm.targetElement !== document.body)) {
 +        s.o.style.top = (screenY+scrollY-storm.flakeHeight)+'px';
 +      } else if (storm.flakeBottom) {
 +        s.o.style.top = storm.flakeBottom+'px';
 +      } else {
 +        s.o.style.display = 'none';
 +        s.o.style.bottom = '0%';
 +        s.o.style.position = 'fixed';
 +        s.o.style.display = 'block';
 +      }
 +    };
 +
 +    this.vCheck = function() {
 +      if (s.vX>=0 && s.vX<0.2) {
 +        s.vX = 0.2;
 +      } else if (s.vX<0 && s.vX>-0.2) {
 +        s.vX = -0.2;
 +      }
 +      if (s.vY>=0 && s.vY<0.2) {
 +        s.vY = 0.2;
 +      }
 +    };
 +
 +    this.move = function() {
 +      var vX = s.vX*windOffset, yDiff;
 +      s.x += vX;
 +      s.y += (s.vY*s.vAmp);
 +      if (s.x >= screenX || screenX-s.x < storm.flakeWidth) { // X-axis scroll check
 +        s.x = 0;
 +      } else if (vX < 0 && s.x-storm.flakeLeftOffset < -storm.flakeWidth) {
 +        s.x = screenX-storm.flakeWidth-1; // flakeWidth;
 +      }
 +      s.refresh();
 +      yDiff = screenY+scrollY-s.y+storm.flakeHeight;
 +      if (yDiff<storm.flakeHeight) {
 +        s.active = 0;
 +        if (storm.snowStick) {
 +          s.stick();
 +        } else {
 +          s.recycle();
 +        }
 +      } else {
 +        if (storm.useMeltEffect && s.active && s.type < 3 && !s.melting && Math.random()>0.998) {
 +          // ~1/1000 chance of melting mid-air, with each frame
 +          s.melting = true;
 +          s.melt();
 +          // only incrementally melt one frame
 +          // s.melting = false;
 +        }
 +        if (storm.useTwinkleEffect) {
 +          if (s.twinkleFrame < 0) {
 +            if (Math.random() > 0.97) {
 +              s.twinkleFrame = parseInt(Math.random() * 8, 10);
 +            }
 +          } else {
 +            s.twinkleFrame--;
 +            if (!opacitySupported) {
 +              s.o.style.visibility = (s.twinkleFrame && s.twinkleFrame % 2 === 0 ? 'hidden' : 'visible');
 +            } else {
 +              s.o.style.opacity = (s.twinkleFrame && s.twinkleFrame % 2 === 0 ? 0 : 1);
 +            }
 +          }
 +        }
 +      }
 +    };
 +
 +    this.animate = function() {
 +      // main animation loop
 +      // move, check status, die etc.
 +      s.move();
 +    };
 +
 +    this.setVelocities = function() {
 +      s.vX = vRndX+rnd(storm.vMaxX*0.12,0.1);
 +      s.vY = vRndY+rnd(storm.vMaxY*0.12,0.1);
 +    };
 +
 +    this.setOpacity = function(o,opacity) {
 +      if (!opacitySupported) {
 +        return false;
 +      }
 +      o.style.opacity = opacity;
 +    };
 +
 +    this.melt = function() {
 +      if (!storm.useMeltEffect || !s.melting) {
 +        s.recycle();
 +      } else {
 +        if (s.meltFrame < s.meltFrameCount) {
 +          s.setOpacity(s.o,s.meltFrames[s.meltFrame]);
 +          s.o.style.fontSize = s.fontSize-(s.fontSize*(s.meltFrame/s.meltFrameCount))+'px';
 +          s.o.style.lineHeight = storm.flakeHeight+2+(storm.flakeHeight*0.75*(s.meltFrame/s.meltFrameCount))+'px';
 +          s.meltFrame++;
 +        } else {
 +          s.recycle();
 +        }
 +      }
 +    };
 +
 +    this.recycle = function() {
 +      s.o.style.display = 'none';
 +      s.o.style.position = (fixedForEverything?'fixed':'absolute');
 +      s.o.style.bottom = 'auto';
 +      s.setVelocities();
 +      s.vCheck();
 +      s.meltFrame = 0;
 +      s.melting = false;
 +      s.setOpacity(s.o,1);
 +      s.o.style.padding = '0px';
 +      s.o.style.margin = '0px';
 +      s.o.style.fontSize = s.fontSize+'px';
 +      s.o.style.lineHeight = (storm.flakeHeight+2)+'px';
 +      s.o.style.textAlign = 'center';
 +      s.o.style.verticalAlign = 'baseline';
 +      s.x = parseInt(rnd(screenX-storm.flakeWidth-20),10);
 +      s.y = parseInt(rnd(screenY)*-1,10)-storm.flakeHeight;
 +      s.refresh();
 +      s.o.style.display = 'block';
 +      s.active = 1;
 +    };
 +
 +    this.recycle(); // set up x/y coords etc.
 +    this.refresh();
 +
 +  };
 +
 +  this.snow = function() {
 +    var active = 0, flake = null, i, j;
 +    for (i=0, j=storm.flakes.length; i<j; i++) {
 +      if (storm.flakes[i].active === 1) {
 +        storm.flakes[i].move();
 +        active++;
 +      }
 +      if (storm.flakes[i].melting) {
 +        storm.flakes[i].melt();
 +      }
 +    }
 +    if (active<storm.flakesMaxActive) {
 +      flake = storm.flakes[parseInt(rnd(storm.flakes.length),10)];
 +      if (flake.active === 0) {
 +        flake.melting = true;
 +      }
 +    }
 +    if (storm.timer) {
 +      features.getAnimationFrame(storm.snow);
 +    }
 +  };
 +
 +  this.mouseMove = function(e) {
 +    if (!storm.followMouse) {
 +      return true;
 +    }
 +    var x = parseInt(e.clientX,10);
 +    if (x<screenX2) {
 +      windOffset = -windMultiplier+(x/screenX2*windMultiplier);
 +    } else {
 +      x -= screenX2;
 +      windOffset = (x/screenX2)*windMultiplier;
 +    }
 +  };
 +
 +  this.createSnow = function(limit,allowInactive) {
 +    var i;
 +    for (i=0; i<limit; i++) {
 +      storm.flakes[storm.flakes.length] = new storm.SnowFlake(parseInt(rnd(flakeTypes),10));
 +      if (allowInactive || i>storm.flakesMaxActive) {
 +        storm.flakes[storm.flakes.length-1].active = -1;
 +      }
 +    }
 +    storm.targetElement.appendChild(docFrag);
 +  };
 +
 +  this.timerInit = function() {
 +    storm.timer = true;
 +    storm.snow();
 +  };
 +
 +  this.init = function() {
 +    var i;
 +    for (i=0; i<storm.meltFrameCount; i++) {
 +      storm.meltFrames.push(1-(i/storm.meltFrameCount));
 +    }
 +    storm.randomizeWind();
 +    storm.createSnow(storm.flakesMax); // create initial batch
 +    storm.events.add(window,'resize',storm.resizeHandler);
 +    storm.events.add(window,'scroll',storm.scrollHandler);
 +    if (storm.freezeOnBlur) {
 +      if (isIE) {
 +        storm.events.add(document,'focusout',storm.freeze);
 +        storm.events.add(document,'focusin',storm.resume);
 +      } else {
 +        storm.events.add(window,'blur',storm.freeze);
 +        storm.events.add(window,'focus',storm.resume);
 +      }
 +    }
 +    storm.resizeHandler();
 +    storm.scrollHandler();
 +    if (storm.followMouse) {
 +      storm.events.add(isIE?document:window,'mousemove',storm.mouseMove);
 +    }
 +    storm.animationInterval = Math.max(20,storm.animationInterval);
 +    storm.timerInit();
 +  };
 +
 +  this.start = function(bFromOnLoad) {
 +    if (!didInit) {
 +      didInit = true;
 +    } else if (bFromOnLoad) {
 +      // already loaded and running
 +      return true;
 +    }
 +    if (typeof storm.targetElement === 'string') {
 +      var targetID = storm.targetElement;
 +      storm.targetElement = document.getElementById(targetID);
 +      if (!storm.targetElement) {
 +        throw new Error('Snowstorm: Unable to get targetElement "'+targetID+'"');
 +      }
 +    }
 +    if (!storm.targetElement) {
 +      storm.targetElement = (document.body || document.documentElement);
 +    }
 +    if (storm.targetElement !== document.documentElement && storm.targetElement !== document.body) {
 +      // re-map handler to get element instead of screen dimensions
 +      storm.resizeHandler = storm.resizeHandlerAlt;
 +      //and force-enable pixel positioning
 +      storm.usePixelPosition = true;
 +    }
 +    storm.resizeHandler(); // get bounding box elements
 +    storm.usePositionFixed = (storm.usePositionFixed && !noFixed && !storm.flakeBottom); // whether or not position:fixed is to be used
 +    if (window.getComputedStyle) {
 +      // attempt to determine if body or user-specified snow parent element is relatlively-positioned.
 +      try {
 +        targetElementIsRelative = (window.getComputedStyle(storm.targetElement, null).getPropertyValue('position') === 'relative');
 +      } catch(e) {
 +        // oh well
 +        targetElementIsRelative = false;
 +      }
 +    }
 +    fixedForEverything = storm.usePositionFixed;
 +    if (screenX && screenY && !storm.disabled) {
 +      storm.init();
 +      storm.active = true;
 +    }
 +  };
 +
 +  function doDelayedStart() {
 +    window.setTimeout(function() {
 +      storm.start(true);
 +    }, 20);
 +    // event cleanup
 +    storm.events.remove(isIE?document:window,'mousemove',doDelayedStart);
 +  }
 +
 +  function doStart() {
 +    if (!storm.excludeMobile || !isMobile) {
 +      doDelayedStart();
 +    }
 +    // event cleanup
 +    storm.events.remove(window, 'load', doStart);
 +  }
 +
 +  // hooks for starting the snow
 +  if (storm.autoStart) {
 +    storm.events.add(window, 'load', doStart, false);
 +  }
 +
 +  return this;
 +
 +}(window, document));
 +</file>
 +++++
 +===== Полезные команды =====
 +Включить превью офисных форматов:
 +<code bash>
 +nano /var/www/html/config/config.php
 +</code>
 +<code>
 +'preview_libreoffice_path' => '/usr/bin/libreoffice',
 +</code>
 +
 +Перемещение файлов от одного пользователя к другому
 +<code bash>
 +sudo -u www-data php /var/www/html/occ files:transfer-ownership --move user-from user-to
 +</code>
 +https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html#transfer
 +
 +Удалить пользователя username:
 +<code bash>
 +sudo -u www-data php /var/www/html/occ user:delete username
 +</code>
 +
 +Почистить корзину у всех пользователей
 +<code bash>
 +sudo -u www-data php /var/www/html/cloud/occ trashbin:cleanup --all-users
 +</code>
 +===== Решение проблем =====
 +==== Failed to connect to www.nextcloud.com ====
 +В логах куча сообщений:\\
 +//GuzzleHttp\Exception\ConnectException: cURL error 7: Failed to connect to www.nextcloud.com port 80: Connection timed out//
 +
 +Сайт nexcloud.com реально бывает недоступен. Workaround - отключить проверку на наличие интернета:
 +<code bash>
 +echo "'has_internet_connection' => false," >> /var/www/html/config/config.php
 +</code>
 +Или не обращать внимания.
 +
 +==== Some files have not passed the integrity check ====
 +После обновления - ошибка подписи файлов:\\
 +//Some files have not passed the integrity check. Further information on how to resolve this issue can be found in the documentation. (List of invalid files… / Rescan…)//
 +
 +Помимо выполнения [[https://docs.nextcloud.com/server/14/admin_manual/issues/code_signing.html#fixing-invalid-code-integrity-messages|рекомендаций]], убедиться, что core/signature.json актуальный.
 +
 +==== Поломались "красивые" ссылки (без index.php) ====
 +Обновить файл .htaccess:
 +<code bash>
 +sudo -u www-data php /var/www/html/occ maintenance:update:htaccess
 +</code>
 +https://docs.nextcloud.com/server/14/admin_manual/installation/source_installation.html#pretty-urls
 +
 +==== Specified key was too long; max key length is 767 bytes ====
 +При обновлении Nexcloud ошибка:\\
 +<color #ed1c24>DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ENGINE = InnoDB ROW_FORMAT = compressed': SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes</color>
 +
 +Решение:
 +<code bash>
 +mysql -u root -p cloud
 +
 +MariaDB [cloud]> set global innodb_large_prefix=on;
 +MariaDB [cloud]> set global innodb_file_format=Barracuda;
 +quit
 +
 +sudo -u www-data php /var/www/html/occ maintenance:repair
 +sudo -u www-data php /var/www/html/occ upgrade
 +</code>
 +
 +==== The database is missing some indexes ====
 +Проверка в админке пишет: The database is missing some indexes
 +
 +Решение:
 +<code bash>
 +sudo -u www-data php /var/www/html/occ db:add-missing-indices
 +# В докере:
 +docker exec -u www-data nc php occ db:add-missing-indices
 +</code>
 +
 +==== Обновление прошло неуспешно, PHP грузит систему на 100%, сайт в неотключаемом maintenance mode ====
 +Отключить регулярную задачу в crontab.\\
 +<code bash>
 +# Проверить, включен ли apc cli:
 +php -i | grep apc.enable
 +  apc.enable_cli => Off => Off
 +  apc.enabled => On => On
 +# Если нет, то включить
 +echo "apc.enable_cli=1" >> /etc/php/7.4/cli/php.ini
 +# Перейти в каталог NC (обязательно!) и запустить апгрейд заново
 +cd /var/www/html/cloud
 +sudo -u www-data php occ upgrade
 +</code>
 +Включить регулярную задачу в crontab.
 +
 +https://help.nextcloud.com/t/nextcloud-21-update-needed/108714/25
 +
 +==== "Module php-imagick in this instance has no SVG support. For better compatibility it is recommended to install it ====
 +<code bash>
 +# Для Alpine. Для контейнера добавить этот пакет в докерфайл.
 +apk add imagemagick-svg
 +# Старое решение
 +apt-get install libmagickcore-6.q16-6-extra
 +</code>
 +https://www.reddit.com/r/NextCloud/comments/11fb4rs/module_phpimagick_in_this_instance_has_no_svg/\\
 +https://help.nextcloud.com/t/how-to-enable-svg-for-php-imagick/108646/4
 +
 +
 +
 +==== Your installation has no default phone region set ====
 +<code php>
 +sudo -u www-data php /var/www/html/occ config:system:set default_phone_region --value="RU"
 +# Докер
 +docker exec -u www-data nc php occ config:system:set default_phone_region --value="RU"
 +</code>
 +
 +==== Last background job execution ran 15 hours ago. Something seems wrong ====
 +Запустить принудительно
 +<code bash>
 +sudo -u www-data php -f /var/www/html/cloud/cron.php
 +</code>
 +
 +==== PHP Fatal error:  Out of memory (allocated 3533701120) (tried to allocate 36864 bytes) in /var/www/html/lib/private/AppFramework/Utility/SimpleContainer.php on line 133 ====
 +При выполнении PHP cron.
 +
 +I had APCu activated in the nextcloud config.php. It seems I didn’t have ''apc.enable_cli=1'' in ''/etc/php/8.0/cli/conf.d/20-apcu.ini''. After I added that line to the file occ worked and there was no segmentation error anymore.
 +
 +https://help.nextcloud.com/t/solved-occ-command-php-fatal-error-allowed-memory-size-of-xxx-bytes-exhausted/108521/16
 +
 +==== Docker - The reverse proxy header configuration is incorrect ====
 +Warning: The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy. If not, this is a security issue and can allow an attacker to spoof their IP address as visible to the Nextcloud. Further information can be found in the documentation.
 +<code bash>
 +docker exec -uwww-data nc-php php /var/www/html/cloud/occ config:system:set forwarded_for_headers 0 --value="X-Forwarded-For"
 +# docker exec -uwww-data nc-php php /var/www/html/cloud/occ config:system:set forwarded_for_headers 1 --value="HTTP_X_FORWARDED_FOR"
 +
 +# Сейчас NC не ориентируется на DNS-имя прокси, ему подавай IP, иначе статус красный.
 +# Решение - добавить подсеть, причём, единственным пунктом
 +docker exec -uwww-data nc-php php /var/www/html/cloud/occ config:system:set trusted_proxies 0 --value="172.16.0.0/12"
 +# docker exec -uwww-data nc-php php /var/www/html/cloud/occ config:system:set trusted_proxies 1 --value=reverse-proxy
 +</code>
 +https://github.com/nextcloud/docker/issues/800\\
 +https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/reverse_proxy_configuration.html
 +
 +==== Docker - exec: "php": executable file not found in $PATH ====
 +После обновления контейнера PHP с версии 7.4 на 8.0 при попытке выполнить команду php occ внутри контейнера выдаётся ошибка:\\
 +OCI runtime exec failed: exec failed: unable to start container process: exec: "php": executable file not found in $PATH: unknown
 +
 +Проблема в том, что в дистрибутиве Alpine автоматически не создаётся символическая ссылка php8 -> php, т. к. для php8 не готовы все пакеты. Решение: добавить в Dockerfile
 +<code bash>
 +ln -sf /usr/bin/php8 /usr/bin/php # -f - перезаписывать, если она уже есть (после предыдущего создания)
 +</code>
 +
 +==== The __Host prefix mitigates cookie injection ====
 +https://scan.nextcloud.com: The __Host prefix mitigates cookie injection vulnerabilities within potential third-party software sharing the same second level domain. It is an additional hardening on top of 'normal' same-site cookies.
 +
 +Предположительное решение: https://github.com/DoTheEvo/selfhosted-apps-docker/issues/7
 +
 +==== Клиент виснет после начала синхронизации ====
 +Ситуация - виснет клиент практически сразу после запуска, нагружает процессор, настройки открыть невозможно, в логах ничего внятного, переустановка на разные версии, удаление служебных файлов в каталоге синхронизации ничего не даёт.
 +
 +Решение:
 +<code powershell>
 +# убить процесс
 +Get-Process nextcloud |kill
 +# запустить синхронизацию с помощью nextcloudcmd
 +"$env:programfiles\Nextcloud\nextcloudcmd.exe" --silent `
 +"$env:userprofile\Nextcloud" https://bva.dyndns.info/cloud
 +</code>
 +Затем запустить клиента, зайти в Настройки -> Сеть и убрать ограничения на скорость загрузки/передачи.
 +
 +https://docs.nextcloud.com/desktop/latest/advancedusage.html#nextcloud-command-line-client
 +
 +==== Операция не разрешена модулем контроля доступа ====
 +Operation is blocked by access control
 +
 +Симптом - маленькие файлы грузятся, а больше примерно 10 МБ - нет. Решение - разрешить на реверс-прокси url c ''/.''
 +
 +https://help.nextcloud.com/t/nextcloud-17-nginx-unable-to-upload-files-larger-than-unknown-size/67624/2
 +
 +==== X-Robots-Tag HTTP header is not set ====
 +<color #ff7f27>The "X-Robots-Tag" HTTP header is not set to "noindex, nofollow". This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.</color>
 +
 +Поменять опцию в ''nginx/conf.d/default.conf''
 +<code bash>
 +add_header X-Robots-Tag  "none"               always;
 +# на
 +add_header X-Robots-Tag  "noindex, nofollow"  always;
 +</code>
 +
 +Because “none” is indeed equivalent to “noindex, nofollow” for Google, but seems to be not supported by Bing and probably other search engines. (https://help.nextcloud.com/t/x-robots-tag-http-header-not-configured-with-noindex-nofollow-since-nc-26-0-0/158300)
 +
 +==== OPcache interned strings buffer is nearly full ====
 +<color #ff7f27>The OPcache interned strings buffer is nearly full. To assure that repeating strings can be effectively cached, it is recommended to apply opcache.interned_strings_buffer to your PHP configuration with a value higher than 8.</color>
 +
 +<code bash>
 +sed -i '/opcache.interned_strings_buffer =/c opcache.interned_strings_buffer = 16/' /etc/php/8.2/fpm/php.ini
 +systemctl restart php-fpm
 +</code>
 +
 +==== OnlyOffice не работает после обновления Nextcloud на версию 28 ====
 +В логе
 +<code>
 +Exception array_merge(): Argument #2 must be of type array, null given in file '[...]/lib/public/AppFramework/Http/Response.php' line 273
 +</code>
 +Временное решение: в файле ''lib/public/AppFramework/Http/Response.php'' в строке 273:
 +<code>
 +return array_merge($mergeWith, $this->headers);
 +# заменить на
 +return array_merge($mergeWith, $this->headers?? []);
 +</code>
 +<code bash>
 +sed -i '273s/headers.*/headers?? []);/' /var/www/html/lib/public/AppFramework/Http/Response.php
 +</code>
 +
 +==== Could not check for JavaScript support ====
 +<color #ff7f27>Could not check for JavaScript support. Please check manually if your webserver serves `.mjs` files using the JavaScript MIME type.</color>
 +
 +https://docs.nextcloud.com/server/latest/admin_manual/installation/nginx.html
 +
 +==== Server has no maintenance window start time configured ====
 +<color #ff7f27>Server has no maintenance window start time configured. This means resource intensive daily background jobs will also be executed during your main usage time. We recommend to set it to a time of low usage, so users are less impacted by the load caused from these heavy tasks.</color>
 +
 +A value of 1 e.g. will only run these background jobs between 01:00am UTC and 05:00am UTC.
 +<code php>
 +'maintenance_window_start' => 1,
 +</code>
 +https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/config_sample_php_parameters.html#maintenance-window-start\\
 +https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/background_jobs_configuration.html
 +
 +==== Expected filesize of X bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) Y bytes ====
 +Проблема закачки файлов в Nextcloud.
 +<code>
 +[no app in context] Error: Expected filesize of 6609487 bytes but read (from Nextcloud client) and wrote (to Nextcloud storage) 4833280 bytes.
 +Could either be a network problem on the sending side or a problem writing to the storage on the server side.
 +</code>
 +Причина в реверс-прокси Traefik, у которого с какого-то момента изменилось умолчание readTimeout с 0 до 60 сек, и он рвёт соединение. Если поставить обратно 0, всё начинает нормально работать.
 +<code bash>
 +--entryPoints.web.transport.respondingTimeouts.readTimeout=0s
 +# Либо добавить переменную в Докере
 +TRAEFIK_ENTRYPOINTS_WEB_TRANSPORT_RESPONDINGTIMEOUTS_READTIMEOUT: "0s"
 +</code>
 +https://github.com/nextcloud/server/issues/37695#issuecomment-2259203772
 +
 +==== Incorrect row format found in your database ====
 +После обновления на версию 31
 +
 +<color #ff7f27>Incorrect row format found in your database. ROW_FORMAT=Dynamic offers the best database performances for Nextcloud. Please update row format on the following list: oc_profile_config, oc_talk_internalsignaling, oc_calendar_appt_bookings, oc_notifications_pushhash, oc_talk_attendees, oc_talk_bridges, oc_accounts_data, oc_circles_event, oc_recent_contact, oc_talk_commands, oc_ratelimit_entries, oc_notifications_settings, oc_calendar_appt_configs, oc_circles_token, oc_circles_membership, oc_talk_rooms, oc_user_status, oc_webauthn, oc_circles_member, oc_circles_remote, oc_authorized_groups, oc_known_users, oc_circles_circle, oc_circles_share_lock, oc_talk_sessions, oc_circles_mount, oc_circles_mountpoint, oc_storages_credentials. For more details see the documentation (https://dev.mysql.com/doc/refman/en/innodb-row-format.html).</color>
 +
 +<code bash>
 +#!/bin/bash
 +
 +DB_USER='root'
 +DB_PASS='P@ssw0rd'
 +DB_NAME='nc'
 +
 +mysql -u "$DB_USER" -p"$DB_PASS" -e "
 +SELECT CONCAT('ALTER TABLE \`', TABLE_NAME, '\` ROW_FORMAT=DYNAMIC;'
 +FROM INFORMATION_SCHEMA.TABLES 
 +WHERE TABLE_SCHEMA = '$DB_NAME' 
 +AND ENGINE = 'InnoDB';
 +" -B -N | while read -r sql; do
 +    mysql -u "$DB_USER" -p"$DB_PASS" -e "$sql" "$DB_NAME"
 +done
 +</code>
 +https://help.nextcloud.com/t/upgrade-to-nextcloud-hub-10-31-0-0-incorrect-row-format-found-in-your-database/218366
 +
 +==== High-performance backend (HPB) ====
 +<color #ed1c24>No High-performance backend configured - Running Nextcloud Talk without the High-performance backend only scales for very small calls (max. 2-3 participants). Please set up the High-performance backend to ensure calls with multiple participants work seamlessly. For more details see the documentation ↗.</color>
 +
 +При домашнем использовании проще отключить это предупреждение в настройках Talk, т. к. вряд ли там будет больше 2-3 участников.
 +
 +В другом случае https://www.google.com/search?q=nextcloud+hpb+docker-compose
 +
 +===== Ссылки =====
 +[[https://nextcloud.com/changelog/|Changelog на сайте производителя]]\\
 +[[https://download.nextcloud.com/server/releases/|Страница со списком релизов]]\\
 +[[https://scan.nextcloud.com/|Сканер безопасности]]\\
 +[[https://nextcloud.com/install/#install-clients|Скачать клиенты для компьютеров и мобильных устройств]]
 +
 +https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki