Toolarity

Syncthing 使い方:競合ファイルと.stignore

トラブル対処Syncthing 公開 2026年10月18日

同期フォルダーを開いたら、notes.md の隣に notes.sync-conflict-20260925-060144-US3TWHL.md ができていた。素直に考えれば、この競合コピーを書いたのはデバイス US3TWHL だと思うはずです。私のテストでは逆でした。名前に入っている ID は、自分の版が勝ったデバイスのもので、コピーの中身は負けたデバイスの編集でした。

競合コピーと .stignore は、Syncthing の動きが予想といちばん食い違いやすい二か所です。そこで Syncthing のインスタンスを二つ用意し、八つのケースをわざと起こしました。両側での同時編集を向きを変えて二通り、編集と削除のぶつかり合い、競合コピーを無効にした場合、そして無視パターンの四つの状況です。以下の結果はすべて、テストログと両側のディレクトリ一覧から取ったものです。

結論(Syncthing v2.1.5、2026-09-25 に検証):

状況起きたこと
接続が切れている間に、両方のデバイスで同じファイルを編集した更新日時が新しいほうの編集が両側で勝った。もう一方の編集は name.sync-conflict-<date>-<time>-<ID>.ext として両方のデバイスに保存された
競合ファイル名の <ID>自分の版が勝ったデバイスの短い ID。コピーの中身を編集したデバイスではない
競合ファイル名の <date>-<time>Syncthing が競合を処理した時刻で、誰かがファイルを編集した時刻ではない。コピー自体は負けた側の元の更新日時を保っている
片方が削除し、もう片方が編集した編集が勝った。削除した側にファイルが戻り、競合コピーは作られなかった
両方のデバイスで maxConflicts を 0 にした負けた編集は破棄された。どちらの側にも競合コピーは残らない
片方のデバイスにだけ .stignore を書いた同期されない。パターンはそのデバイスでしか効かなかった
#include でパターンを共有した動いた。読み込まれるファイルは、ほかのファイルと同じように同期される
無視されたファイルが入ったフォルダーを、別のデバイスが削除したフォルダーは削除されず、同期フォルダーは未同期(Out of Sync)になって「失敗した項目」が一つ出た。パターンに (?d) を付けると解消した
すでに同期済みのフォルダーを無視対象にした中の新しいファイルが同期されなくなり、中のファイルの削除も同期されなくなった。相手側には古いコピーが残った

公式ドキュメントに書いてあること

同期のページ(v2.1.0 向けに書かれたもの、2026-09-24 に確認)は、名前の形式を <filename>.sync-conflict-<date>-<time>-<modifiedBy>.<ext> と説明し、次のように書いています(訳は筆者)。

更新日時が古いほうのファイルが競合ファイルとして扱われ、名前が変更されます。

<modifiedBy> が誰の ID なのかは書かれていません。そこで v2.1.5 のソース(lib/model/folder_sendrecv.go)を確認しました。ローカルのファイルが競合で名前を変えられるとき、接尾辞は受信したファイルの ModifiedBy フィールドから取られます。これは勝った版です。タイムスタンプは、名前を変えるその瞬間に time.Now() で取られます。私のテストはどちらとも一致しました。

無視のページ(v2.1.0 向けに書かれたもの、2026-09-24 に確認)には、.stignore ファイルは「ほかのデバイスに同期されることは決してないが、デバイス間で同期されるファイルを #include することはできる」とあり、(?d) 接頭辞は「そのファイルがディレクトリの削除を妨げている場合に、それらを削除できるようにする」とあります。設定のページは maxConflicts を「任意のファイルについて保持しておく競合コピーの最大数。デフォルトは 10」と説明し、0 にすると「競合コピーを完全に無効にする」と書いています。ただ、0 にすると負けた編集がそのまま消えてしまうことには触れていません。私のテストではそうなりました。

検証環境

  • 一台の Mac 上に Syncthing v2.1.5 のインスタンスを二つ(inst1 と inst2)。公式の darwin-arm64 ビルドを使用。どちらも 127.0.0.1 でだけ待ち受け、「グローバル探索」「LAN内で探索」、中継サーバー経由の通信、NATトラバーサルはすべてオフ。
  • 両者で共有する新しいフォルダー t4 を一つ。両側とも送受信(Send & Receive)、ファイルのバージョン管理はオフ、maxConflicts はデフォルトの 10。
  • 「変更の監視」(Watch for Changes)はオフにし、再スキャンはすべて手動で実行しました。イベントの順番が監視のタイミングに左右されないようにするためです。
  • 本物の競合を起こすため、二つのデバイス間の接続を一時停止し、両側で同じファイルを編集して再スキャンし、それから一時停止を解除しました。
  • ここまでの操作はすべて、REST API を通して Python スクリプトで行いました。スクリプトと完全なログは私自身の検証メモで、以下の結果はそのログから引用しています。

inst1 の短い ID は V3OZYEV で始まり、inst2 は US3TWHL で始まります。

テスト1:競合でどちらが勝つか、ファイル名は何を意味するか

ケース A。 接続が切れている間に、inst1 は caseA.md に「edited on inst1」と書き、更新日時は 05:59:39 でした。inst2 は「edited on inst2」と書き、更新日時は 06:00:39、つまり一分後です。06:01:42 に再接続しました。数秒後、両方のデバイスはこうなっていました。

caseA.md                                         "edited on inst2"   mtime 06:00:39
caseA.sync-conflict-20260925-060144-US3TWHL.md   "edited on inst1"   mtime 05:59:39

新しいほうの編集(inst2)が勝ちました。競合コピーの中身は inst1 のテキストなのに、名前の末尾は US3TWHL、つまり inst2 です。

ケース B はその逆で、今回は inst1 の編集のほうが新しくなっています。

caseB.md                                         "edited on inst1"   mtime 06:00:52
caseB.sync-conflict-20260925-060204-V3OZYEV.md   "edited on inst2"   mtime 05:59:52

ここでも接尾辞は勝った側(inst1、V3OZYEV)で、中身は負けた側のものです。

ここから分かることは三つです。

  1. 競合ファイル名の ID は「勝ったデバイス」と読む。 負けたテキストを誰が書いたかを知りたければ、もう一方のデバイスを見てください。
  2. 名前に入る時刻は競合が処理された時刻(06:01:44、再接続の二秒後)で、誰かが何かを入力した時刻ではありません。コピー自身の更新日時には、元の編集時刻(05:59:39)がそのまま残っています。
  3. 競合コピーは普通のファイルなので、同期されます。 最終的に両方のデバイスにありました。ドキュメントによれば、これは意図した動きです。その競合は「ほかのどこでも同じく競合」だからです。

テスト2:片側で編集、もう片側で削除

ケース C。 接続が切れている間に、inst1 は caseC.md を削除し、inst2 はそれを編集しました。再接続後、両方のデバイスに inst2 が編集した内容の caseC.md があり、競合コピーはどこにもありませんでした。

つまりこのケースでは編集が勝ち、削除したデバイスにファイルが戻ってきました。消したはずのファイルが復活したら、オフラインの間にほかのデバイスがそれを変更していなかったか確認してください。

テスト3:maxConflicts = 0

ケース D。 REST の設定 API で両方のデバイスの maxConflicts を 0 にし(maxConflicts はフォルダーごとの設定です)、ケース A を繰り返しました。今回も inst2 の編集のほうが新しくなっています。

inst1: caseD.md   "edited on inst2"
inst2: caseD.md   "edited on inst2"

それだけです。inst1 のテキスト「edited on inst1」は、どちらのデバイスのどのファイルにもありませんでした。ファイルのバージョン管理はオフだったので、ほかに残しているものもありません。

maxConflicts = 0 は競合を隠す設定ではありません。古いほうの編集が捨てられるという意味です。競合ファイルが煩わしいなら、中身を確認してから削除するか、0 ではなく 1 のような小さい数にしてください。

テスト4:.stignore は書いたデバイスにしか効かない

ケース E。 inst1 の .stignore にだけ *.log と書き、inst1 で e1.log、inst2 で e2.log を作りました。

inst1: caseE.md  e1.log
inst2: caseE.md  e2.log

.stignore 自体は inst2 に届かず、.log ファイルはどちらの方向にも移動しませんでした。inst2 には無視パターンが無いのに、inst1 は e2.log を受け取りませんでした。無視パターンは、該当するファイルの送信と受信の両方を止めます。

テスト5:#include でパターンを共有する

ケース F。 inst1 で、*.bak と書いた .stglobalignore を作りました。普通のファイルなので inst2 に同期されました(存在を確認済み)。そのうえで次のようにしました。

inst1 .stignore:   *.log
                   #include .stglobalignore
inst2 .stignore:   #include .stglobalignore

GET /rest/db/ignores が返した実際に効いているパターンは、inst1 で *.log と *.bak、inst2 で *.bak でした。inst1 で作った f1.bak は inst1 に、inst2 で作った f2.bak は inst2 に留まりました。

これはドキュメントが勧めているやり方です。どこでも効かせたいルールは同期されるファイルに書き、各デバイスの .stignore からそれを #include します。デバイス固有のルールは .stignore 自体に残します。ドキュメントには、読み込むファイルが存在しないとエラーになるとも書かれているので、#include の行を足す前に、共有ファイルを作って同期させておいてください。

テスト6:無視されたファイルがフォルダーの削除を止める、(?d) で直る

フォルダーが止まったまま動かなくなるのは、このケースです。

ケース G。 gdir と hdir という二つのフォルダーに、それぞれ普通のファイルを一つずつ入れて、両方のデバイスに同期しました。inst2 で、それぞれに無視されるファイルを一つずつ置きました。gdir/junk(素の junk パターンで無視)と hdir/junk2((?d)junk2 で無視)です。それから inst1 で両方のフォルダーを削除しました。

  • inst2 では hdir が削除された。junk2 も一緒に消えた。
  • gdir は inst2 に残り、中身は junk だけになった。

inst2 のフォルダーは未同期(Out of Sync)に変わり、「失敗した項目」が一つ出ました。

Syncthing の Web GUI(v2.1.5、日本語表示)。inst2 の「t4」フォルダーのパネルに赤字で「未同期」、「同期の必要な項目」が「1 項目, ~0 B」、「失敗した項目」が「1 項目」と表示されている。「ローカル状態」の下に「無視パターン該当分を除く」とあり、ローカル状態のフォルダー数がグローバル状態より一つ多い。フォルダーパスは伏せ字にしてある。

失敗した項目(Failed Items)の件数をクリックすると、理由がはっきり書かれています。日本語表示にしていても、エラー文そのものは英語のままです。

Syncthing の「失敗した項目」ダイアログ(v2.1.5、日本語表示)。項目「gdir」のエラーとして「syncing: delete dir: directory has been deleted on a remote device but contains ignored files (see ignore documentation for (?d) prefix)」が英語のまま表示されている。

inst1 側では、リモートデバイス inst2 のこのフォルダーの完了率が 95% のまま止まっていました。テストの残りの時間(その後約五分)、自然に解消することはありませんでした。

次に inst2 のパターンを junk から (?d)junk に変えて再スキャンしました。一分もしないうちに gdir は削除され、失敗した項目は消え、inst1 から見た inst2 は再び 100% になりました。つまり、すでに止まっているフォルダーも (?d) を足せば直せます。最初からやり直す必要はありません。ドキュメントが (?d) を勧めているのは、.DS_Store のような OS が勝手に作るファイルです。大事なファイルを守るためのパターンには (?d) を付けないでください。付けると、そのファイルはフォルダーと一緒に削除されます。

テスト7:すでに同期済みのフォルダーを無視する

ケース H。 hold/old.md は両方のデバイスに同期済みでした。その後 inst1 で .stignore に hold/ を足し、hold/new.md を作り、さらに後で hold/old.md を削除しました。

inst2 hold/ after new.md was created on inst1:   old.md
inst2 hold/ after old.md was deleted on inst1:   old.md

new.md が届かなかったのは想定どおりです。分かりにくいのは次の点です。削除も同期されませんでした。 パスが無視対象になると、Syncthing はそのパスについて削除も含めて一切の変更を送らなくなります。そのため、相手のデバイスには手元にあったコピーがそのまま残ります。フォルダーをほかのデバイスから消し、しかもその後は無視したいなら、まず削除し、削除が同期されるのを待ってから、無視パターンを足してください。

私ならこうする

  • 競合ファイルを見つけたら、本体のファイルと見比べて手でマージし、それから競合ファイルを削除する。 接尾辞の ID は勝ったデバイスで、中身は負けた編集です。
  • maxConflicts を 0 にしない。 二つの編集のうち古いほうを失っても構わないと確信できるなら別です。もう一段の安全策が欲しければ、ファイルのバージョン管理をオンにしてください。
  • 共有する無視ルールは同期されるファイルに書き(例えば .stglobalignore)、すべてのデバイスでそれを #include する。.stignore だけでは、それが置かれたデバイスしかカバーできません。
  • OS が作るゴミファイルには (?d) を使う。 そうすれば、削除したフォルダーがほかのデバイスで止まらなくなります。すでに「contains ignored files」という失敗した項目が出ているフォルダーでも、そのパターンに (?d) を足して再スキャンしたら、私のテストでは解消しました。
  • フォルダーをすべての場所から消し、その後も入れたくないときは、先に削除、無視はその後。

この検証で扱っていないこと

  • 検証したのは macOS 上の Syncthing v2.1.5 だけで、一台のマシン上の二つのインスタンスを使い、2026-09-25 に行いました。大文字と小文字を区別しないファイルシステムや、ほかの OS は試していません。
  • フォルダーはすべて送受信でした。受信のみ、送信のみのフォルダーでは、ローカルの変更の扱いが違います。
  • 競合は各ケースで一回ずつ、二つの編集の間隔は一分で試しました。更新日時がまったく同じ編集は試していません。ドキュメントによれば、その場合はデバイス ID で決まります。
  • 編集と削除のケースでは編集が勝ちました。削除が勝つケースは見つからなかったので、ドキュメントにある「削除が勝つ」ときの競合コピーは見ていません。
  • ファイルのバージョン管理は最初から最後までオフでした。