Show HN: DriveSync – 高速なGit風Googleドライブ同期CLI
Show HN: DriveSync – fast Git-styled Google Drive sync CLI (github.com)
要約
DriveSync (dsync) は、ローカルフォルダとGoogleドライブフォルダを同期する、Rust製の高速なコマンドラインツールです。Gitにインスパイアされた設計で、ローカルの変更をプッシュしたり、リモートの変更をプルしたり、差分を確認したりできます。インストールはHomebrewやバイナリ、ソースから可能で、セットアップもOAuthクライアントIDの作成と初期化コマンドで容易に行えます。
全文翻訳
フォルダとファイル
DriveSync (dsync)
ローカルフォルダとGoogle Driveフォルダを同期させる、軽量かつ高速なコマンドラインツールです。ローカルの変更をプッシュしたり、リモートの変更をプルしたり、あるいは単に差分を確認したりできます。Rustで書かれており、odeke-em/drive をモデルにしています。
- ソース: https://github.com/yadvlabs/drivesync
- GUI/メニューバーアプリ by @ScaleNinja: https://scaleninja.com/drivesync/
- ライセンス: MIT; バイナリにリンクされているクレートは独自のライセンスを保持しており、THIRD-PARTY-NOTICES.md に記載されています。
テスト済み: macOSで、コンシューマーアカウント(
@gmail.com)とGoogle Workspaceアカウントの両方で動作します。LinuxおよびWindowsビルドはCIで生成・テストされていますが、実際のDriveでの動作確認は行われていません。フィードバックをお待ちしています。
dsync init ~/gdrive --remote-folder backups/lab --credentials ~/Downloads/client_secret.json
cd ~/gdrive
dsync diff # 差分を確認
dsync push # ローカルの変更をアップロード(計画を表示し、確認を求めます)
dsync pull # リモートの変更をダウンロード
なぜこれなのか?
rcloneのような汎用ツールと比較して、dsync は意図的に範囲を絞っています。ローカルフォルダ1つ、Driveフォルダ1つ、単一の静的Rustバイナリです。dsync が異なる点:
- 何かを変更する前に計画を表示し、確認を求めます。両方のサイドで変更された場合は、
--forceなしでは競合として扱われ、上書きされません。 - デフォルトでは決して削除しません。
--deleteオプションを使用すると、DriveのエントリはDriveのゴミ箱に、ローカルファイルはmacOSではゴミ箱に、Windowsではごみ箱に移動されます(削除 を参照)。 - 繰り返し実行は低コストです。リモートインデックスは、再リストする代わりにDrive Changes APIを通じて常に最新の状態に保たれ、ローカルMD5はgitのようにサイズとmtimeをキーとするSQLiteインデックスにキャッシュされます。
- コンテンツの比較はデフォルトでMD5を使用します(Driveがサーバーサイドで計算します)。
--fastはrsyncのように、サイズとmtimeが同じであれば信頼します。--verifyはキャッシュを信頼せず、すべてのローカルファイルを再ハッシュします。 - すべての転送は、転送後にDriveのMD5と比較して検証されます。ダウンロードは一時ファイルに着地し、検証後にリネームされて配置されます。
- 並列転送とハッシュ(デフォルト8ストリーム、最大64)。再起動しても継続可能なアップロード。
- gitignore構文を使用する
.driveignoreファイルと、フォルダ内のどこからでも実行できるgitスタイルのコマンド。 - ドライブされるように構築されています。
--jsonはJSONイベント(計画、プロンプト、バイト単位の進捗、結果)をストリームし、すべてのエラーには安定したコードが付与されるため、GUIやスクリプトは prose(自然言語)を解析する必要がありません。
インストール
Homebrew (macOS および Linux):
brew install scaleninja/tap/drivesync
これにより scaleninja/tap タップが追加されるため、他のscaleninjaツールは brew install <name> でインストールできます。(Homebrewが「信頼できないタップ」エラーを返す場合は、まず brew trust scaleninja/tap を実行してください。)
または、LinuxまたはmacOS(x86_64およびarm64)用のプリビルドバイナリを リリースページ から入手してください:
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) asset=linux-x86_64 ;; Linux-aarch64) asset=linux-arm64 ;;
Darwin-x86_64) asset=macos-x86_64 ;; Darwin-arm64) asset=macos-arm64 ;;
*) echo "unsupported platform"; exit 1 ;;
esac
curl -fL -o /tmp/dsync "https://github.com/scaleninja/drivesync/releases/latest/download/dsync-$asset" \
&& chmod 755 /tmp/dsync && sudo mv /tmp/dsync /usr/local/bin/dsync
Windows (x86_64): リリースページ から dsync-windows-x86_64.exe をダウンロードし、dsync.exe にリネームして PATH のあるフォルダに配置してください。Windowsに関する注意点は 既知の制限事項 を参照してください。
または、Rustツールチェーンを使用してソースから: git clone https://github.com/scaleninja/drivesync && cd drivesync && make install。
セットアップ
dsync は、ユーザーが作成する OAuthクライアントを使用してDriveと通信するため、バイナリに認証情報は組み込まれていません。 Google Cloud Console で一度だけ行う手順があります(スクリーンショット付きのステップバイステップガイド があります)。
- プロジェクトを作成(または既存のものを選択)します。
- APIとサービス → ライブラリ: Google Drive API を有効にします。
- APIとサービス → OAuth同意画面: 外部 を選択し、アプリ名(同意画面に表示される名前)を入力し、サポートメールアドレスを記入し、テストユーザー として自分を追加します。
- APIとサービス → 認証情報 → 認証情報を作成 → OAuth クライアント ID: デスクトップアプリ を選択し、JSONをダウンロードします。
その後、フォルダを初期化します。ブラウザが開いて同意を求められます。ツールはループバックポートでコードを受け取り、トークンをフォルダ内の .gd/ に保存します(Unixではモード 0600、アップロードされません)。
dsync init ~/gdrive --remote-folder backups/lab --credentials ~/Downloads/client_secret.json
Googleは、テスト中 の外部アプリの更新トークンを7日間で期限切れにします。毎週
initを再実行するように求められる場合は、同意画面で アプリを公開 をクリックしてください(個人の使用には検証は不要です)。
使用方法
同期フォルダ内のどこからでもコマンドを実行できます。PATH は現在のディレクトリからの相対パスで、ファイルまたはフォルダを指定できます。デフォルトは . です。
| コマンド | 説明 |
|---|---|
init [DIR] --remote-folder P --credentials F | 認証を行い、DIR を My Drive/P をミラーリングする同期フォルダに設定します。--depth N はレベルを制限します(-1 = 無制限)。--no-browser は認証URLを表示します。 |
push [PATH] | 新しい、またはローカルで更新されたファイルをアップロードします。計画を表示し、確認を求めます。 |
pull [PATH] | Driveで新しい、または更新されたファイルをダウンロードします。計画を表示し、確認を求めます。 |
diff [PATH] | 変更日時とともに、差分を表示します。何も変更しません。差分がある場合は終了ステータス1で終了します。 |
status [--check] | ローカルフォルダ、リモートフォルダ、深さ、キャッシュ状態、無視ファイル、ファイルシステムの種類、トークン有効期限を表示します。--check はDriveにも接続し、認証情報とリモートフォルダを検証して、サインインしているアカウントとストレージ使用量を表示します。 |
update-cache | リモートツリーのインデックスのみを更新します。 |
version | バージョンを表示します。 |
push、pull、diff のオプション:
| オプション | 効果 |
|---|---|
-y, --no-prompt | 確認なしで適用します。計画に競合が含まれる場合は拒否されます。 |
--force | 同一mtimeで宛先が新しい、またはコンテンツが異なる競合も上書きします。大文字小文字の衝突は強制されません。 |
-j N, --threads N | 並列ストリーム数(デフォルト8、最大64)。 |
--refresh | キャッシュされたインデックスを無視し、リモートツリー全体を再リストします。 |
--fast | rsyncスタイルで高速チェック:サイズとmtimeが同じであれば、ファイルを読み込まずに信頼します。 |
--verify | ハッシュが必要なすべてのファイルを、ローカルハッシュキャッシュを信頼せずに再読み込みします。 |
--dry-run | push/pull のみ。計画を表示して、確認や変更なしで終了します。変更がある場合は終了ステータス1、計画にエラーがある場合(読み込めないローカルファイルなど)は2、それ以外は0で終了します。 |
--skip-conflicts | push/pull のみ。計画に競合がある場合、それ以外を転送します。--no-prompt と一緒に使用すると、拒否する代わりに、通常のプロンプトで対話的に処理します。 |
--delete | push/pull のみ。転送後、ソースに存在しなくなったものを宛先から削除します。push はDriveエントリをDriveのゴミ箱に移動します。pull はmacOSではローカルファイルをゴミ箱に、Windowsではごみ箱に移動し、Linuxでは削除します。デフォルトではオフです(削除 を参照)。 |
同期ルートにある .driveignore ファイルに gitignore スタイルのパターンを記述して、除外するファイル(init は .DS_Store と ._* を含むものを生成します)を指定できます。.gd/、シンボリックリンク、および正規ファイル以外のファイルは同期されません。
終了ステータスは、成功時は0、diff が差分を見つけた場合や --dry-run が変更を見つけた場合は1、エラーまたは転送の失敗があった場合は2です。すべてエラーメッセージの末尾には、角括弧で囲まれた安定したコードが付与されます(例: error: not inside a dsync workspace [not_workspace])。コードは 機械可読出力 でリストされています。PATH はディスク上のスペル通りに解釈されるため、macOSでは push Docs と push docs は同じフォルダを意味します。ワークスペースでは一度に1つの dsync コマンドのみ実行され、2番目のコマンドは待機します。
機械可読出力
--json(コマンドの前または後に指定可能)を使用すると、すべてのコマンドが標準出力に1行に1つのJSONオブジェクトを出力し、標準エラー出力には何も出力しないため、GUIやスクリプトは prose(自然言語)を解析せずに実行を追跡できます(--help と --version はプレーンテキストのままです)。各オブジェクトには event フィールドがあります。
| イベント | 時期 | フィールド |
|---|---|---|
phase | ステップの開始(スキャン、リスト、アップロードなど)または実行が別のインスタンスを待機しているとき | message |
plan | 計画された各エントリ | kind(add, modify, delete, skip, conflict, error)、path、dir、size、note。削除は side を追加。modify, skip, conflict, error は reason(下記)を追加。 |
summary | 計画の後 | additions, modifications, deletions(各 {count, bytes})、skips, errors, conflicts |
note | 説明的な行(競合、削除、リトライ、実行中に変更されたファイル) | text |
outcome | 何もすることがない場合 | outcome: up_to_date, nothing_to_transfer, in_sync |
prompt | dsync が標準入力からの y/n を待っているとき | question, default_yes, deletions, conflicts |
progress | 1ファイルあたりの移動バイト数、最大1MiBまたは250msごと、0から開始 | path, bytes, total(不明な場合はnull)。リトライ後、bytes はDriveが認識したバイト数まで戻ることがあります。5MB以下のファイルは0と報告され、その後フルサイズが報告されます。 |
result | 1つのアイテムが完了したとき | path, dir, outcome(uploaded, updated, downloaded, mkdir, trashed, deleted, gone, skipped, failed)、detail |
done | 実行が完了したとき(failures > 0 の場合は常に最後のイベント、終了ステータス2) | command, changes, failures; dry_run: true は changes が実行されるであろうカウントの場合。declined: true は「いいえ」の後。 |
warning | 非致命的な問題 | message; 一部の警告には code(unreadable, unmappable, special_file, deletions_skipped)と path または name が付与される。 |
error | 実行が完了前に停止したとき(常に最後のイベント) | code(下記)、message、関連する場合はカウントも。 |
diff, diff_summary | dsync diff | change(local_only, remote_only, local_newer, remote_newer, modified, type_mismatch, unreadable, case_collision)、path、local、remote。その後 differ, counts。 |
status, account | dsync status | テキスト形式と同じ情報。--check の後には account(email, name, storage_usage, storage_limit, remote_folder_ok)が付与される。 |
auth_url, initialized | dsync init | url; root, remote_folder, remote_folder_id |
cache, version | update-cache, version | entries, was, updated_at; version |
パスはワークスペース相対で / 区切り、末尾にスラッシュはありません(dir はフォルダかどうかを示します)。そのため、2つのスペースを含む名前は、テキスト形式とは異なり、曖昧さがありません。
plan イベントの reason は、note の単語による説明に対応する安定した識別子であり、リーダーは独自のテキストをレンダリングできます(note は英語で変更される可能性がありますが、reason は変更されません)。add および delete エントリには存在しません。値は、重なり合う部分では diff の語彙を共有します。
kind | reason の値 |
|---|---|
modify | local_newer, remote_newer(ソースが新しい)、forced_destination_newer(--force で新しいコピーを上書き)、modified_same_mtime(同じmtimeでコンテンツが異なる) |
conflict | local_newer, remote_newer(宛先が新しい)、modified_same_mtime, case_collision |
skip | native_doc_not_overwritten, native_doc_no_export, native_doc_not_trashed, type_mismatch(片方がフォルダで片方がファイル)、type_mismatch_below(上記パスの下、--delete のみ)、outside_sync(ローカルでシンボリックリンク、特殊ファイル、または無視されたフォルダとして存在、--delete のみ)、unreadable_folder(読み込めないローカルフォルダの下、--delete のみ) |
error | unreadable |
エラーコードは、error イベントおよびすべてのテキストエラーの末尾の角括弧内に存在します。
| コード | 意味 |
|---|---|
not_workspace | 初期化されたフォルダの外にいる |
no_credentials | フォルダに保存された認証情報がない。dsync init を実行してください。 |
auth_expired | 更新トークンが無効になったか期限切れになった。dsync init を実行してください(セットアップ の テスト中 アプリに関する注意を参照)。 |
auth_failed | その他の認証失敗。成功したトークン更新後の401や、付与自体に関する403(同意画面でのスコープ削除、APIが無効)を含む。 |
remote_folder_missing | リモートフォルダが削除、ゴミ箱移動、移動、または置き換えられた |
path_invalid | パスがワークスペース外、予約済み、除外済み、シンボリックリンク、または欠落している |
conflicts | --no-prompt で競合が発生した(残りを転送するには --skip-conflicts を渡してください)。 |
delete_refused | --delete で空のソースが拒否された |
transfer_failed | 実行は完了したが、一部のアイテムが失敗した(failures, total)。 |
api | Driveがリクエストを拒否したか、到達できなかった |
io | ローカルファイルシステムまたはデータベースのエラー |
usage | 引数が不正 |
internal | その他すべて |
典型的なドライバーは dsync --json push を実行し、plan イベントが到着するたびにレンダリングし、prompt イベントに標準入力に y または n と改行を書き込んで応答し、その後 progress および result イベントを done まで追跡します。--dry-run はプロンプトなしで計画を生成します。
何が変更されたかを判断する方法
各サイドは、パス、サイズ、mtime、MD5のセットです。DriveはサーバーサイドでMD5を計算してリストに含めるため、リモートサイドは自由です。ファイルは最も安価なチェックから順に比較されます。
- サイズが異なる → 異なる。何も読み込まれません。
- それ以外の場合、ローカルMD5が比較されます。これは一度計算され、ファイルのサイズとmtimeに対してキャッシュされるため、最初の実行後は、statが変更されたファイルのみが再度読み込まれます(
--verifyはキャッシュをバイパスします。--fastはサイズとmtimeが一致する場合はハッシュをスキップします)。 - MD5が同じ → 同一。mtimeに関係なく。
- MD5が異なる → 新しい方のサイド(1秒の許容範囲)が勝ちます。同じmtimeでコンテンツが異なる場合は競合です。
転送後、両方のサイドは同じmtimeを持つため、diff は何も報告しません。リモートツリーは一度全体がインデックス化され、その後Drive Changes APIを通じて最新の状態に保たれるため、繰り返し実行してもDriveを再リストする必要はありません。
プッシュとプルは、変更ごとに1行を表示し、実行前に確認を求めます。
+ photos/2026/
+ photos/2026/a.jpg 1,234,567 B
M docs/report.txt 12,340 B, local newer
! docs/Budget skipped: remote is a Google-native document; not overwritten
C notes.txt conflict: remote is newer
Addition count 2 src: 1,234,567 B
Modification count 1 src: 12,340 B
Proceed with the changes? [Y/n]:
+ 作成 · M 上書き · ! 構造的な理由でスキップ · C 競合 · E 読み込めないローカルファイル · D 削除(--delete のみ)。標準入力がターミナルでない場合、質問の後に改行が続くため、パイプで dsync を駆動するプログラムは完全な行を見ることができます。
決して行わないこと
- 何も削除しません。どちらのサイドでも、
--deleteオプションで明示的に要求されない限り。ローカルで削除されたファイルはDriveに残りますし、その逆も同様です。削除 を参照して、--deleteが何をするか、およびその保護について確認してください。 - 競合を上書きしません。
--forceなしでは、または名前の衝突(大文字小文字やUnicode正規化によるもの、フォールディングするファイルシステムの場合)も決して行いません。競合が存在する場合、プロンプトのデフォルトは いいえ であり、--no-promptは実行を拒否し、入力終了は はい とはみなされません。 - 古いデータを書き込みません。書き込み直前に各宛先が再チェックされます。ローカルファイルは計画時に見たサイズとmtimeを保持している必要があります。Driveファイルは、計画時に見たMD5、mtime、名前、親フォルダを保持している必要があります。作成されるファイルはまだ存在しない必要があります。同期ツリー内でインデックスが配置した場所に座っている必要があります。その間に変更されたものはすべて報告され、そのままにされます。読み込み中またはアップロード中に変更されたローカルファイルはエラーとして報告され、すべてのアップロードはDriveが計算したMD5に対して検証されます。
- 同期フォルダから脱出することはありません。シンボリックリンク、
.gd/内、無視されたパス上、または正規ファイル以外のものには何も書き込まれません。ダウンロードはプライベートな一時ファイルに行われ、DriveのMD5に対して検証された後、既存のパーミッションを維持してリネームされて配置されます。 - 重複を作成しません。各作成の直前に、同じ名前のエントリについてDriveに問い合わせ、作成にはDriveが事前に生成したIDを使用するため、応答損失後のリトライや古いインデックスによって、2番目のコピーが作成されたり、他人のファイルが採用されたりすることはありません。Driveが既に重複を保持している場合、最も古いものが一貫してパスを所有します。大きなアップロードは、Driveが受信した最後のバイトから再開されます。セッションは、ファイルのサイズとmtimeが変更されない限り、
.gd/に保持されます。 - Googleドキュメント、スプレッドシート、スライドには対応しません。バイナリコンテンツがなく、常にスキップされます。
5MBを超えるファイルは、継続可能なアップロードを通じてストリームされます。レート制限、ネットワークエラー、接続の停止はバックオフ付きでリトライされます。Ctrl-Cで中断されたプッシュまたはプルは、再実行するだけで済みます。リモートフォルダがDriveでゴミ箱に移動されたり削除されたりした場合、すべてのコマンドは停止してその旨を通知します。破損した .gd/cache.db は自動的に再構築されます。これは単なるインデックスです。
--delete を使用した削除
rsync --delete のように、push --delete および pull --delete は、転送後、ソースに存在しなくなったものを宛先から削除します。移動先は以下の通りです。
| 宛先 | --delete が行うこと | |
|---|---|---|
push | Google Drive | エントリをDriveのゴミ箱に移動します。Driveから一時的に復元可能です。 |
pull on macOS | ローカルフォルダ | ファイルをゴミ箱に移動します(Finderの元に戻すエントリはありません。ドラッグして復元してください)。ゴミ箱が使用できない場合、ファイルはそのまま残り、実行は失敗を報告します。 |
pull on Windows | ローカルフォルダ | ファイルをごみ箱に移動します。使用できない場合、ファイルはそのまま残り、実行は失敗を報告します。 |
pull on Linux | ローカルフォルダ | 永久に削除します。Linuxにはゴミ箱との統合はありません。 |
保護機能:
- 削除は、
D行として計画にリストされ、独自のカウントを持ちます。削除が含まれる場合、プロンプトのデフォルトは いいえ です。--no-promptは確認なしで適用します。無人実行に--deleteを渡すことは、計画が削除するものをすべて受け入れることを意味します。 - 空のソースは拒否されます。ソースサイドに選択されたフォルダ自体以外に何も保持されていない場合、
--deleteは宛先をクリアするため、コマンドは何も実行せずに停止します。 - 削除は最後に行われ、すべての転送が成功した場合にのみ実行されます。失敗が発生した場合、それらはスキップされ報告されるため、部分的な実行でコピーに失敗したものを削除することはありません。
- 各削除は、実行時に再チェックされます。Driveエントリは、計画時に見た名前、親、コンテンツ、変更日時を保持している必要があり、Driveフォルダは空である必要があります。ローカルファイルは、計画時に見たサイズとmtimeを保持している必要があります。シンボリックリンクをたどることはなく、ローカルフォルダは空の場合にのみ削除されます。その間に変更されたものはそのままにされ、報告されます。
- 決して削除されないもの: Googleドキュメント、スプレッドシート、スライド。大文字小文字またはUnicode正規化のみで異なる名前(ケースフォールディングファイルシステム上)。一方ではフォルダ、他方ではファイルであるパスの下にあるもの。読み込めなかったローカルフォルダの下にあるもの。
.driveignoreで除外されたもの。プッシュ時、同期がカバーしない形式(シンボリックリンク、特殊ファイル、または無視ルールで除外されたフォルダ)でローカルに存在するものはすべて。そのようなエントリ(.DS_Store、Googleドキュメント、または設定された深さ以上のコンテンツ)をまだ含むフォルダは保持され、スキップとして報告されます。また、所有者がゴミ箱に移動できる、他人が所有するDriveファイルも同様です。
既知の制限事項
- 同じDriveフォルダを同期する2台のマシンは連携していません。3者マージはありません。Drive APIには条件付き更新がないため、リモートファイルが計画時に見たものであることを確認するチェックと、それに続く書き込み(または
--deleteを使用したゴミ箱へのリクエスト)は2つのリクエストであり、そのウィンドウ中に別の場所からの書き込みは失われます。 - プルはローカルバックアップを保持しません。上書きするローカルファイル(古いもののみ、
--forceを除く)は失われます。--deleteで削除するものはmacOSではゴミ箱、Windowsではごみ箱に移動され、Linuxでは削除されます。プッシュはDriveコンテンツを置き換えますが、Driveは一時的にリビジョンを保持し、削除ではなくゴミ箱に移動します。 - 完全なリストは、ツリーが組み立てられる間、すべてのMy Driveエントリをメモリに保持します。
dsyncと同時に同期フォルダに書き込んでいる別のローカルプロセスは、サポートされている脅威モデルの範囲外です。宛先は各書き込みの直前にチェックされますが、それと同時にアトミックではありません。- 同期フォルダはローカルディスク上に置いてください。NFSまたはSMB共有では、ワークスペースロックもSQLiteインデックスも信頼性がありません。
- ハッシュキャッシュは、gitのように、変更されていないサイズとmtimeを信頼します。ファイルをバックアップから復元した後や、mtimeを保持したままファイルを書き換えるツールがある場合は、
--verifyを使用してください。 - Windows:
.gd/内のトークンとキャッシュファイルには厳格なパーミッションが設定されていません(Unixモードビットがないため)。ユーザープロファイルACLに依存してください。260文字を超えるパスは、システム全体でロングパスが有効になっていない限りサポートされません。Windowsでファイル名として禁止されているDrive名(`<>: