Nextcloud で 4GB 以上の大容量ファイルをアップロードする設定

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69711/7df64cbf-88c0-6c35-e2b0-73cfe5cf34e0.png" />

[*Nextcloud - Wikipedia*](https://ja.wikipedia.org/wiki/Nextcloud#/media/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB:Nextcloud_Logo.svg)

[だいぶ前に作成した ownCloud](https://qiita.com/bbq-all-stars/items/7828eda324ae4bd23dd8) を、最近 Nextcloud に置き換えた。
置き換えたのは良いのだが、どうも 4GB 以上の大容量ファイルのアップロードが何回やっても失敗していてどうにもならない。
どうなってんだー！ということで、issue を漁りまくって設定を見直したら何とか 10GB 以上のファイルのアップロードもできました、というお話。

以下、発生したエラーと対応方法を書きます。


# 前提
* サーバースペック
    * AWS Lightsail
    * 2 GB RAM
    * 1 vCPU
    * 60 GB SSD
* Nextcloudバージョン
    * 23.0.2
* 公式の docker-compose.yml で環境を作成
    * https://github.com/nextcloud/docker/tree/master/.examples/docker-compose/with-nginx-proxy/mariadb/fpm
    * nginx (proxy) + nginx (web) + Nextcloud (php-fpm) + mariaDB + redis の全部入り
* S3 をプライマリストレージとして使用

# 発生したエラーと対処方法
以下の設定方法は、docker-compose で作成した環境を前提としている。
それ以外の環境は各自で読み替えてください。

## PHP のファイルサイズの上限を上げる
PHP の設定がデフォルトだと 512MB 以上のファイルがアップロードできないので、設定を変更する。
docker の設定で、PHP_MEMORY_LIMIT と PHP_UPLOAD_LIMIT という設定があるので、それを追加する。
設定値はお好みで。

```diff
diff --git a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/docker-compose.yml b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/docker-compose.yml
index 33b3d92..d89864d 100644
--- a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/docker-compose.yml
+++ b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/docker-compose.yml
@@ -24,6 +24,16 @@ services:
     environment:
       - MYSQL_HOST=db
       - REDIS_HOST=redis
       - OVERWRITEPROTOCOL=https
       - OBJECTSTORE_S3_HOST=s3.ap-northeast-1.amazonaws.com
       - OBJECTSTORE_S3_BUCKET=hogehoge-drive
       - OBJECTSTORE_S3_KEY=AAAAAAAAAAAAAAAA
       - OBJECTSTORE_S3_SECRET=aaaaaaaaaaaaaaaaaaaaaaaaaa
       - OBJECTSTORE_S3_PORT=443
       - OBJECTSTORE_S3_SSL=true
       - OBJECTSTORE_S3_REGION=ap-northeast-1
+      - PHP_MEMORY_LIMIT=1G
+      - PHP_UPLOAD_LIMIT=5G
     env_file:
       - db.env
     depends_on:
```

PHP_MEMORY_LIMIT は設定しなくても動くけど、自分の環境はメモリに余裕があったので設定した。


## nginx のファイルサイズ上限を上げる
proxy と web それぞれの nginx の設定も変更しないと大容量ファイルのアップロードができない。
こちらもデフォルトだと 512MB までなので、それぞれ対応する設定を変更する。

proxy の設定変更
```diff
diff --git a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/proxy/uploadsize.conf b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/proxy/uploadsize.conf
index 7e3906e..eb05553 100644
--- a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/proxy/uploadsize.conf
+++ b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/proxy/uploadsize.conf
@@ -1,2 +1,6 @@
-client_max_body_size 10G;
+client_max_body_size 15G;
 proxy_request_buffering off;
```

web の設定変更
```diff
diff --git a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/web/nginx.conf b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/web/nginx.conf
index 8fbc162..6af9b77 100644
--- a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/web/nginx.conf
+++ b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/web/nginx.conf
@@ -42,7 +42,7 @@ http {
         #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;

         # set max upload size
-        client_max_body_size 512M;
+        client_max_body_size 15G;
         fastcgi_buffers 64 4K;

         # Enable gzip but do not remove ETag headers
```

## 各種タイムアウトの設定
ここからが本題。
これまでの設定を反映させて再度同期を行ったところ、3GB 程度のファイルはアップロードできるが、だいたい 4GB 以上のファイルになると以下のようなエラーが出るようになった。

> Error when assembling chunks, status code 504

> チャンクを組み立てる際のエラー、ステータスコード504

Nextcloud は大容量ファイルをアップロードする際にファイルを分割して送るのだが、どうもアップロード後に分割したファイルを PHP 側で結合する際に時間がかかり、その間に nginx 側でタイムアウトしてしまうことによるものらしい。
また、PHP の実行自体に時間がかかると、そこでもタイムアウトが発生して PHP の実行がキャンセルされてしまうらしい。

（参考）
https://help.nextcloud.com/t/error-when-assembling-chunks-status-code-504/56751/6
https://github.com/nextcloud/server/issues/17992
https://github.com/linuxserver/docker-nextcloud/issues/211

そのため、nginx 側のタイムアウト値の上限と、PHP の実行時間の上限を引き上げてやる必要がある。
nginx 側のタイムアウト値は、 proxy と web それぞれを設定する必要がある。

タイムアウト値はお好みで。
ここでは 30 分で設定している。

### proxy の設定変更
```diff
diff --git a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/proxy/uploadsize.conf b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/proxy/uploadsize.conf
index 7e3906e..eb05553 100644
--- a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/proxy/uploadsize.conf
+++ b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/proxy/uploadsize.conf
@@ -1,2 +1,6 @@
 client_max_body_size 15G;
 proxy_request_buffering off;
+proxy_connect_timeout 1800;
+proxy_send_timeout 1800;
+proxy_read_timeout 1800;
+send_timeout 1800;
```

### web の設定変更
```diff
diff --git a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/web/nginx.conf b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/web/nginx.conf
index 8fbc162..6af9b77 100644
--- a/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/web/nginx.conf
+++ b/.examples/docker-compose/with-nginx-proxy/mariadb/fpm/web/nginx.conf
@@ -144,6 +144,11 @@ http {

             fastcgi_intercept_errors on;
             fastcgi_request_buffering off;
+
+            fastcgi_read_timeout 1800;
+            fastcgi_send_timeout 1800;
+            fastcgi_connect_timeout 1800;
```

### PHP の実行時間の変更
PHP の実行時間はそのままでは変更できないため、docker コンテナの中に入って PHP の設定ファイルを書き換える必要がある。

まずは docker コンテナの中に入る。
```bash
$ sudo docker exec -it fpm-app-1 sh
```

コンテナ内でエディターをインストールして、PHP の設定ファイルを開く。
```bash
$ apk add vim
$ vim /usr/local/etc/php/conf.d/nextcloud.ini
```

PHP の設定ファイルを開いたら、以下の行を追加。
```diff
--- nextcloud.ini
+++ /usr/local/etc/php/conf.d/nextcloud.ini
@@ -1,3 +1,5 @@
 memory_limit=${PHP_MEMORY_LIMIT}
 upload_max_filesize=${PHP_UPLOAD_LIMIT}
 post_max_size=${PHP_UPLOAD_LIMIT}
+max_execution_time=1800
+max_input_time=1800
```

## S3 へのマルチパートアップロードの並列度調整
タイムアウト値を変更してアップロードしたところ、今度はこんな感じのエラーが出るようになった。

```
An exception occurred while uploading parts to a multipart upload. The following parts had errors:
- Part 1: Error executing \"UploadPart\" on \"https://s3.eu-central-1.wasabisys.com/example.com/urn%3Aoid%3A6899?partNumber=1&uploadId=0p_vUl_cL8R-z7o7JQacOPTjP3Vhqy_sVxwGv8CRVa39uzWZqBC_IrD0wfP9d0sLVAX_uGkLalq-Fhguy7FgJcHspt9ZeeZ3yzqYEcUbmVhY1ONPX5X57BVpRajusER6\"; AWS HTTP error: Client error: `PUT https://s3.eu-central-1.wasabisys.com/example.com/urn%3Aoid%3A6899?partNumber=1&uploadId=0p_vUl_cL8R-z7o7JQacOPTjP3Vhqy_sVxwGv8CRVa39uzWZqBC_IrD0wfP9d0sLVAX_uGkLalq-Fhguy7FgJcHspt9ZeeZ3yzqYEcUbmVhY1ONPX5X57BVpRajusER6` resulted in a `400 Bad Request` response:
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Error><Code>RequestTimeout</Code><Message>Your socket connection to the server w (truncated...)
 RequestTimeout (client): Your socket connection to the server was not read from or written to within the timeout period, source:  read tcp 130.117.252.13:443->78.46.193.122:35876: i/o timeout - <?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Error><Code>RequestTimeout</Code><Message>Your socket connection to the server was not read from or written to within the timeout period, source:  read tcp 130.117.252.13:443-&gt;78.46.193.122:35876: i/o timeout</Message><RequestId>0800D03C011B289E</RequestId><HostId>4K4Kd98tbwzYAQ9Of04l+YWtRoC4ykRJcznDiTbqFljFzHuqkVVIFAGYqpOTqzaNvYqrHRXFJk6m</HostId></Error>
- Part 2: Error executing ...
```

どうやら S3 へのマルチパートアップロード時にタイムアウトが発生しているっぽい。

PR を漁ると、Nextcloud で使用されている AWS SDK for PHP は、並列度を指定することで並列マルチパートでアップロードができるのだが、サーバーのスペックが不足していると負荷がかかり、S3 へのアップロードでタイムアウトを起こしてしまうとのこと。

（参考）
https://github.com/nextcloud/server/pull/24330

そのため、並列度をデフォルトの 5 ではなく、それよりも数を減らすと良いらしい。
v23.0.2 の Nextcloud では、並列度の変更ができないので、ソースの PHP を直接修正する。

再度 docker コンテナの中に入る。
```bash
$ sudo docker exec -it fpm-app-1 sh
```

コンテナに入ったら、該当処理のソースを開く。
```bash
$ vim lib/private/Files/ObjectStore/S3ObjectTrait.php
```

マルチパートアップロードをしている処理に、以下のような concurrency の設定を追加する。
```diff
--- ./S3ObjectTrait_original.php
+++ lib/private/Files/ObjectStore/S3ObjectTrait.php
@@ -110,11 +110,10 @@
        protected function writeMultiPart(string $urn, StreamInterface $stream, string $mimetype = null): void {
                $uploader = new MultipartUploader($this->getConnection(), $stream, [
                        'bucket' => $this->bucket,
                        'key' => $urn,
                        'part_size' => $this->uploadPartSize,
+                       'concurrency' => 1,
                        'params' => [
                                'ContentType' => $mimetype
                        ],
                ]);
```

自分の場合は、1 にしたら、エラーが全く出なくなり、無事に 10GB 以上のファイルも同期できるようになった。
うれしい。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69711/b7dcbf09-8533-3cce-b51d-65d9da09ec9f.png" />

# おわりに
Nextcloud に乗り換えを決めた当初は、公式で docker-compose.yml あるじゃんサイコー！！と思って嬉々として環境を作ったのだが、その後にサイズが大きなファイルの同期ができないことがわかって、数日間ずっと試行錯誤することになってしまった。
ファイルサイズが大きいと1回の試行にも時間がかかるので、途中完全に萎えてしまったのだが、なんとか 10GB 超のファイルも同期できるようになって良かったなと。