Chienomi

NextCloudを廃止してSeafileを採用

Live With Linux::server

去年NextCloudを組んで、その後MEGA廃止の影響で割とちゃんと使うようになったのだけど、結構ずっと不満を言っていた。

最終的に大きな不満としては、全体的にお行儀が悪いということで

  • 意図的な設定であっても「推奨」を「エラー」として扱って無限に警告を吐く
  • ログを溢れさせる
  • クライアントアプリは終了しても勝手に起動する
  • 同期の競合制御が怪しくて「転送をスキップしたのにconflictする」とか「更新したのに反映されない」とかが多い
  • 無駄にリソースを食う
  • モバイルアプリは過剰にバックグラウンド動作や通知を使う

といったことが気に入らなかった。

このため、サーバーマイグレーションに伴ってNextCloudは廃止に踏み切った。

既にLWMPをmTLS経由で使うというのを試しているので、モバイル環境に関してはそれで解決であるとも言え、またファイル共有に関しては最悪Unisonを使うような方法もあるのだが、調べていたらSeafileというものを見つけたので使ってみることにした。

Seafileと他の候補

Seafile

Seafile Ltd.によるファイル共有ソフトウェア。 ファイル同期とファイル共有を提供する。

OSS版と商用ライセンス版がある。 商用ライセンス版はオフィスファイルを編集できたりするらしい。

NextCloudより軽量で的を絞ったソフトウェア。

FileRun

ポルトガルのFileRun, LDAによるクラウドドライブ。 デスクトップ同期もある。

かなりおしゃれな見た目をしている。

商用ソフトウェアで99EUR。 Dockerイメージがあるが、本体は含まれておらず、購入してzipを入手する方式。

あんまりメンテナンスされてない感じがする。

Unison

ファイル同期ツール。 inotifyを使ったfsmonitorがあるので、それで同期することもできる。

diffアルゴリズムが若干微妙なのか、MEGAと比べて正しく判定されないことがある。

シンプルな作りなのは素晴らしいけれど、中断にやや弱いという面もあり、割と微妙。 運用の工夫でカバーできる部分は多いが、MEGAの代替としてはきつい。

この記事の意義

Seafileの構築・運用は本当に罠がいっぱいなので。

Seafileの概要

  • Self-hostedなファイル同期サーバー
  • ファイル同期・共有ができる
  • ファイルブラウザや各種ビューワもあるけど、そんなに強力ではない
  • 基本Dockerで立てる
  • サーバーはPython/Djangoなアプリ
  • メジャーバージョンごとに構成/設定ファイルが相当ドラスティックに変わる
  • モバイルアプリがある
    • 基本はクラウドドライブクライアント
    • フォルダバックアップもある
    • モバイルアプリのほうは画像ビューワもまあまあ使える
    • ただし、オーディオはストリーミング再生はできずダウンロードできるだけ
    • ビデオもストリーミング再生はできず全体ダウンロードしてから再生開始
    • モバイルウェブからなら普通にストリーミング再生できる
  • fuseでストレージ全体をマウントする機能がある
    • readonly
    • おそらく名前をマッピングしているだけで、かなり高速にアクセスできる
    • これにより、davfsを使わなくて済む
  • E2EEがある
    • かなり微妙な動作をするNextCloudと違って使いやすい
    • 同期フォルダ単位でパスワードを設定する方式
  • 「テキストファイル」として扱うファイル拡張子を設定可能
  • 共有は有効期限やパスワードを設定することが可能。内部ユーザーのみ、指定ユーザーのみ、誰でもの権限制御も可能
  • Wiki機能がある
    • Notionみたいな感じ
    • Notionより軽いので圧倒的に使いやすい
    • ウェブのみ。アプリからは利用できない
  • OSSなCommunity Editionと商用(年間サブスクリプション)のPro Editionがある
    • Pro Editionも3ユーザーまでは無料
    • 9ユーザーまでは100USD固定
    • 10ユーザー以上はユーザーあたりでUSBまたはEURの価格が設定されている
    • Pro Editionの機能はほとんどが複数ユーザー
    • Community Editionのほうが取り回しがいいまである
    • SaaSになっているPlus Editionもある

Seafileの構築

順番にやっていけるものではないので、基本的に全部やる。

Docker環境の構築

Seafile 13のものである。 前述のようにSeafileはメジャーバージョンアップでドラスティックに設定が変わるので、13にしか適用できない内容であると思われる。

私は/opt/seafile/*にseafile関連のボリュームをまとめている。 Docker関連は/opt/seafile/docker/

すべてがそのままでは行かないので、順番に説明する。 まず、.env

#################################
# Docker compose configurations #
#################################
COMPOSE_FILE='seafile-server.yml,seadoc.yml'
COMPOSE_PATH_SEPARATOR=','

## Images
SEAFILE_IMAGE=seafileltd/seafile-mc:13.0-latest
SEAFILE_DB_IMAGE=mariadb:10.11
SEAFILE_REDIS_IMAGE=redis
#SEAFILE_CADDY_IMAGE=lucaslorentz/caddy-docker-proxy:2.9-alpine
SEADOC_IMAGE=seafileltd/sdoc-server:2.0-latest
NOTIFICATION_SERVER_IMAGE=seafileltd/notification-server:13.0-latest
MD_IMAGE=seafileltd/seafile-md-server:13.0-latest

## Persistent Storage
BASIC_STORAGE_PATH=/opt/seafile
SEAFILE_VOLUME=$BASIC_STORAGE_PATH/seafile-data
SEAFILE_MYSQL_VOLUME=$BASIC_STORAGE_PATH/seafile-mysql/db
#SEAFILE_CADDY_VOLUME=$BASIC_STORAGE_PATH/seafile-caddy
SEADOC_VOLUME=$BASIC_STORAGE_PATH/seadoc-data

#################################
#      Startup parameters       #
#################################
SEAFILE_SERVER_HOSTNAME=your.domain.tld
SEAFILE_SERVER_PROTOCOL=https
TIME_ZONE=Asia/Tokyo
JWT_PRIVATE_KEY=YourJwtPrivateKey

#####################################
# Third-party service configuration #
#####################################

## Database
SEAFILE_MYSQL_DB_HOST=db
SEAFILE_MYSQL_DB_USER=seafile
SEAFILE_MYSQL_DB_PASSWORD=your_db_password
SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet_db
SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db
SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=seahub_db

## Cache
CACHE_PROVIDER=redis # or memcached

### Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=

### Memcached
MEMCACHED_HOST=memcached
MEMCACHED_PORT=11211

######################################
#        Initial variables           #
# (Only valid in first-time startup) #
######################################

## Database root password, Used to create Seafile users
INIT_SEAFILE_MYSQL_ROOT_PASSWORD=initial_root_password

## Seafile admin user
INIT_SEAFILE_ADMIN_EMAIL=valid_email_address
INIT_SEAFILE_ADMIN_PASSWORD=initial_password

############################################
# Additional configurations for extensions #
############################################

## SeaDoc service
ENABLE_SEADOC=true

## Notification
ENABLE_NOTIFICATION_SERVER=false
NOTIFICATION_SERVER_URL=

## Seafile AI
ENABLE_SEAFILE_AI=false
SEAFILE_AI_LLM_URL=
SEAFILE_AI_LLM_KEY=

## Metadata server
MD_FILE_COUNT_LIMIT=100000

まず重要な概念。

Seafileは自前のDocker networkを使うようになっている。 そして、標準ではCaddyを使って80:80443:443をexposeする。

別にこの構成のまま使えないこともないのだが、うちはSeafile専用サーバーではないため、Nginxでルーティングする必要がある。 そうなると、Caddyは外すのが普通という話になる。

そこで、.envの時点でCOMPOSE_FILEからCaddyを外している。

続いてseafile-server.yml

services:
  db:
    image: ${SEAFILE_DB_IMAGE:-mariadb:10.11}
    container_name: seafile-mysql
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=${INIT_SEAFILE_MYSQL_ROOT_PASSWORD:-}
      - MYSQL_LOG_CONSOLE=true
      - MARIADB_AUTO_UPGRADE=1
    volumes:
      - "${SEAFILE_MYSQL_VOLUME:-/opt/seafile-mysql/db}:/var/lib/mysql"
    networks:
      - seafile-net
    healthcheck:
      test:
        [
          "CMD",
          "/usr/local/bin/healthcheck.sh",
          "--connect",
          "--mariadbupgrade",
          "--innodb_initialized",
        ]
      interval: 20s
      start_period: 30s
      timeout: 5s
      retries: 10

  redis:
    image: ${SEAFILE_REDIS_IMAGE:-redis}
    container_name: seafile-redis
    restart: unless-stopped
    command:
      - /bin/sh
      - -c
      - redis-server --requirepass "$$REDIS_PASSWORD"
    environment:
      - REDIS_PASSWORD=${REDIS_PASSWORD:-}
    networks:
      - seafile-net

  seafile:
    image: ${SEAFILE_IMAGE:-seafileltd/seafile-mc:13.0-latest}
    container_name: seafile
    restart: unless-stopped
    ports:
      - "8812:80"
      - "8814:8082"
      - "8815:8080"
    volumes:
      - ${SEAFILE_VOLUME:-/opt/seafile-data}:/shared
      - type: bind
        source: /srv/seafile-fuse
        target: /seafile-fuse
        bind:
          propagation: rshared
      - /opt/seafile/docker/patched-entrypoint.sh:/scripts/enterpoint.sh
    privileged: true
    cap_add:
      - SYS_ADMIN
    environment:
      - SEAFILE_MYSQL_DB_HOST=${SEAFILE_MYSQL_DB_HOST:-db}
      - SEAFILE_MYSQL_DB_PORT=${SEAFILE_MYSQL_DB_PORT:-3306}
      - SEAFILE_MYSQL_DB_USER=${SEAFILE_MYSQL_DB_USER:-seafile}
      - SEAFILE_MYSQL_DB_PASSWORD=${SEAFILE_MYSQL_DB_PASSWORD:?Variable is not set or empty}
      - INIT_SEAFILE_MYSQL_ROOT_PASSWORD=${INIT_SEAFILE_MYSQL_ROOT_PASSWORD:-}
      - SEAFILE_MYSQL_DB_CCNET_DB_NAME=${SEAFILE_MYSQL_DB_CCNET_DB_NAME:-ccnet_db}
      - SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=${SEAFILE_MYSQL_DB_SEAFILE_DB_NAME:-seafile_db}
      - SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=${SEAFILE_MYSQL_DB_SEAHUB_DB_NAME:-seahub_db}
      - TIME_ZONE=${TIME_ZONE:-Etc/UTC}
      - INIT_SEAFILE_ADMIN_EMAIL=${INIT_SEAFILE_ADMIN_EMAIL:-me@example.com}
      - INIT_SEAFILE_ADMIN_PASSWORD=${INIT_SEAFILE_ADMIN_PASSWORD:-asecret}
      - SEAFILE_SERVER_HOSTNAME=${SEAFILE_SERVER_HOSTNAME:?Variable is not set or empty}
      - SEAFILE_SERVER_PROTOCOL=${SEAFILE_SERVER_PROTOCOL:-http}
      - SITE_ROOT=${SITE_ROOT:-/}
      - NON_ROOT=${NON_ROOT:-false}
      - JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:?Variable is not set or empty}
      - SEAFILE_LOG_TO_STDOUT=${SEAFILE_LOG_TO_STDOUT:-false}
      - ENABLE_SEADOC=${ENABLE_SEADOC:-true}
      - SEADOC_SERVER_URL=${SEAFILE_SERVER_PROTOCOL:-http}://${SEAFILE_SERVER_HOSTNAME:?Variable is not set or empty}/sdoc-server
      - CACHE_PROVIDER=${CACHE_PROVIDER:-redis}
      - REDIS_HOST=${REDIS_HOST:-redis}
      - REDIS_PORT=${REDIS_PORT:-6379}
      - REDIS_PASSWORD=${REDIS_PASSWORD:-}
      - MEMCACHED_HOST=${MEMCACHED_HOST:-memcached}
      - MEMCACHED_PORT=${MEMCACHED_PORT:-11211}
      - ENABLE_NOTIFICATION_SERVER=${ENABLE_NOTIFICATION_SERVER:-false}
      - INNER_NOTIFICATION_SERVER_URL=${INNER_NOTIFICATION_SERVER_URL:-http://notification-server:8083}
      -  NOTIFICATION_SERVER_URL=${NOTIFICATION_SERVER_URL:-${SEAFILE_SERVER_PROTOCOL:-http}://${SEAFILE_SERVER_HOSTNAME:?Variable is not set or empty}/notification}
      - ENABLE_SEAFILE_AI=${ENABLE_SEAFILE_AI:-false}
      - SEAFILE_AI_SERVER_URL=${SEAFILE_AI_SERVER_URL:-http://seafile-ai:8888}
      - SEAFILE_AI_SECRET_KEY=${JWT_PRIVATE_KEY:?Variable is not set or empty}
      - MD_FILE_COUNT_LIMIT=${MD_FILE_COUNT_LIMIT:-100000}
    labels: []
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - seafile-net

networks:
  seafile-net:
    name: seafile-net

ほとんどデフォルトのままだが、caddyを外すためlabelsを削除している。

デフォルトではCaddyがexposeするので、Caddyを外したために

    ports:
      - "8812:80"
      - "8814:8082"
      - "8815:8080"

の部分が追加されている。

80はSeafile本体、8082はseafhttpのもの、8080はseafdavのもの。

      - type: bind
        source: /srv/seafile-fuse
        target: /seafile-fuse
        bind:
          propagation: rshared
      - /opt/seafile/docker/patched-entrypoint.sh:/scripts/enterpoint.sh
    privileged: true
    cap_add:
      - SYS_ADMIN

この部分はseafile-fuse用。ほとんどの人は必要ない。

seadoc.ymlも同様にcaddy外し。

services:

  seadoc:
    image: ${SEADOC_IMAGE:-seafileltd/sdoc-server:2.0-latest}
    container_name: seadoc
    volumes:
      - ${SEADOC_VOLUME:-/opt/seadoc-data/}:/shared
    ports:
      - "8813:80"
    environment:
      - DB_HOST=${SEAFILE_MYSQL_DB_HOST:-db}
      - DB_PORT=${SEAFILE_MYSQL_DB_PORT:-3306}
      - DB_USER=${SEAFILE_MYSQL_DB_USER:-seafile}
      - DB_PASSWORD=${SEAFILE_MYSQL_DB_PASSWORD:?Variable is not set or empty}
      - DB_NAME=${SEADOC_MYSQL_DB_NAME:-seahub_db}
      - TIME_ZONE=${TIME_ZONE:-Etc/UTC}
      - JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:?Variable is not set or empty}
      - NON_ROOT=${NON_ROOT:-false}
      - SEAHUB_SERVICE_URL=${SEAFILE_SERVICE_URL:-http://seafile}
    labels: []
    depends_on:
      db:
        condition: service_healthy
    networks:
      - seafile-net

networks:
  seafile-net:
    name: seafile-net

こっちの80はseadocのもの。 こちらもexposeがいる。

これでdocker compose up -dで起動できる。

Nginx

Nginxもだいぶ複雑。 あくまでうちの場合。

########## sf (Seafile) ##########

upstream seafile {
  server 127.0.0.1:8812;
  keepalive 32;
}

proxy_cache_path /var/cache/nginx_seafile levels=1:2 keys_zone=seafile_cache:10m max_size=3g inactive=120m use_temp_path=off;

server {
  include global/base.conf;
  include global/sslcert.conf;
  server_name domain.tld;
  access_log /var/log/nginx/domain.tld-80.log;
  location / {
    return 301 https://domain.tld$request_uri;
  }
}

server {
  include global/base.conf;
  include global/sslconfig.conf;
  server_name domain.tld;

  # Cert
  ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/domain.tld/chain.pem;

  access_log /var/log/nginx/domain.tld.log;

  add_header Strict-Transport-Security "max-age=63072000; preload";

  location / {
    proxy_pass http://127.0.0.1:8812;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Connection "upgrade";
    proxy_http_version 1.1;

    proxy_set_header Upgrade $http_upgrade;
    proxy_redirect http://127.0.0.1:8812/ https://domain.tld/;
    proxy_read_timeout 1200s;

    client_max_body_size 0;
  }

  location /seafdav {
    rewrite ^/seafdav$ /seafdav/ permanent;
  }

  location /seafdav/ {
      proxy_pass         http://127.0.0.1:8815/seafdav/;
      proxy_set_header   Host $host;
      proxy_set_header   X-Real-IP $remote_addr;
      proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header   X-Forwarded-Host $server_name;
      proxy_set_header   X-Forwarded-Proto $scheme;
      proxy_read_timeout  1200s;
      client_max_body_size 0;
  }

  location /:dir_browser {
      proxy_pass         http://127.0.0.1:8815/:dir_browser;
  }

  location /seafhttp {
    rewrite ^/seafhttp(.*)$ $1 break;
    proxy_pass http://127.0.0.1:8814;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_connect_timeout  36000s;
    proxy_read_timeout  36000s;
    proxy_send_timeout  36000s;

    send_timeout  36000s;
    client_max_body_size 0;
  }

  location /sdoc-server/ {
    proxy_pass         http://127.0.0.1:8813/;
    proxy_redirect     off;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Host  $server_name;

    client_max_body_size 100m;
  }

  location /socket.io {
    proxy_pass http://127.0.0.1:8813;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_redirect off;

    proxy_buffers 8 32k;
    proxy_buffer_size 64k;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;
  }
}

seafhttpの設定がいるかどうかなのだけど、実際はいらない。 が、うちではよくわからない問題が出た(おそらくインスタンスのダウンロードが止まることが原因だと思うが、そうとは断定できない)ため、一応設定している。

私はいらないと思っている。

seahub_settings.py

seafile-data/seafile/conf/seahub_settings.pyにSeafileの設定がある。 もともと全然書かれていないけれど、ここに設定をちゃんと入れる必要がある。

# -*- coding: utf-8 -*-
SECRET_KEY = "generated_string"

TIME_ZONE = 'Asia/Tokyo'
CSRF_TRUSTED_ORIGINS = ['https://your.domain.tld']
SERVICE_URL = 'https://your_domain.tld'
FILE_SERVER_ROOT = 'https://your_domain.tld/seafhttp'

EMAIL_USE_TLS = True
EMAIL_HOST = 'your.smtp.tld'
EMAIL_HOST_USER = 'your_mail_user'
EMAIL_HOST_PASSWORD = 'your_mail_password'
EMAIL_PORT = 587
DEFAULT_FROM_EMAIL = 'email_address'
SERVER_EMAIL = DEFAULT_FROM_EMAIL

同期だけなら設定しなくても通ったりする部分もある。 ただ、CSRF_TRUSTED_ORIGINSは書かないと通らなかった。

eメール関連はウェブアプリから設定はできない。

seafdav

seafdavを使いたい場合は、seafile-data/seafile/conf/seadav.confenabledtrueに。

seafile-fuse

私はseafile-fuseを必要とする。 なのでセットアップした。

事前準備として/etc/fuse.confuser_allow_otherしておく。 また、/srv/seafile-fusehttp:httpでマウントしたいので

mkdir /srv/seafile-fuse
chown http:http !!$

という感じ。

/opt/seafile/docker/patched-entrypoint.shにコンテナ内の/scripts/entrypoint.shをコピーし、

# start server
else
    /scripts/start.py &
fi

のあとに、

# ----- PATCH -----
sleep 5
/opt/seafile/seafile-server-latest/seaf-fuse.sh start -o allow_other,uid=33,gid=33 /seafile-fuse

という感じ。

patched-entrypoint.shに実行権限をつけるのをお忘れなく。

volumesですり替えるようにしているのでこれで起動時にseaf-fuse.shを実行してくれる。

readonlyだし、PCでは同期すればいいだからあまりseafile-fuseを使う機会はないと思う。 LWMPで連携している私としてはまさに望む機能そのもの。 readonlyなウェブベースファイルブラウザを採用している場合も使うといいかもしれない。

クライアント環境

ウェブクライアント

Seafile WebUI

Seafileのメイン。 大部分の機能がウェブUIからしかアクセスできないのはMEGAやDropboxなどと同じ。

ただ、音声ファイルや動画ファイルは別ウィンドウで開く方式なので、ビューワとしての出来はよくない。 なんなら、ファイルマネージャとしての時期も良くない。

このあたりは最低限といった感じ。

クリーン&シンプルで見た目は良い。

デスクトップクライアント

クライアントはデスクトップ用の同期がLinuxにもある。 ArchlinuxだとAURにあって、要ビルド。結構重い。

デスクトップクライアント自体は

  • 同期用のクライアント
  • クラウドドライブとしてアクセスする(ローカルには同期しない)クライアント
  • CLI

の3種類があり、どれもAppImageがある。 ビルドはシステムアップデートで動かなくなるような依存を持っているので、こっちのほうが良いかも。

同期クライアントは、初期設定で設定したディレクトリの下にSeafileというディレクトリを作るという挙動。 同期フォルダを追加した場合は指定したディレクトリの下に同期フォルダと同名のディレクトリを作るという挙動。

モバイルクライアント

Android向けはGoogle Play StoreとF-Droidにある。 直接パッケージダウンロードも可能。

フォルダバックアップとクラウドドライブとしてのアクセスが可能。

iPhone向けはApp Storeにある。

WebDAV

WebDAVのURLはウェブクライアントの「設定」に表示されているけど、基本的にはyour.domain.tld/seafdav

快適とは言い難い。

評価

基本的な評価

リッチなソフトウェアではないが、必要なものは揃っている。

メモリ消費量としては私の環境でおよそ1GB。 参考までにMattermostはおよそ400MB、Discourseがおよそ1GB。

メモリ消費の大部分はキャッシュ。私の構成の場合はRedisである。

「ファイル同期がしたい、共有も欲しい、軽くビューワもあると嬉しい」という用途にちょうど良く、その点でNextCloudよりも優れている。 余計なものがなく余計な挙動が少ない。

また、E2EEがNextCloudより実用的。

一方、セットアップの手間は小さくなく、リソース消費もそれなりにあるため、軽く気軽に建てられるようなものでもない。

同期とデスクトップクライアント

そこまで精度は良くない。特に、削除の検出はちゃんとできるのに削除の反映が甘い。 ウェブ上で消したほうがいい。

翻訳

nameが「氏名」になっている、とか、そういうレベルの微妙なクオリティ。 Steamでマイナーゲームを遊ぶことに慣れている人なら楽勝。

共有

共有は音声と動画は別タブで開かれるビューとほぼ同じ。 画像も同じようなビューになる。

機能性はあまりないが、最低限ちゃんと機能し、見た目はクリーンでなかなか良い。

共有リンクに期限やパスワードも設定できるので、かなり優秀。

Wiki

便利そうに見えるが、テーブルの幅が作った環境に依存する(デバイスやビューポートの幅に関係なくピクセルで覚えている)のが致命的。

また、モバイルウェブからは閲覧しかできない。

RedNotebook問題

RedNotebookは定期的に変化のない更新をするせいもあって、この手のファイル同期では鬼門である。

あまりにも鬼門すぎるので、Git管理に変更した。