Skip to main content

vdsl_sync/infra/
location.rs

1//! Location — 拠点の多態抽象。
2//!
3//! 各拠点は「何があるか」(scan) と「どこにファイルがあるか」(file_root) を知っている。
4//! Local/SSH/Cloud で処理内容が根本的に異なるため、trait による多態で実装を切り替える。
5//!
6//! # 層配置
7//!
8//! `Location` trait は infra層に配置する。
9//! 理由: 実装が RemoteShell, StorageBackend, ContentHasher 等の infra型に依存するため。
10//! Domain層の `LocationId` は値オブジェクト(識別子のみ)として残る。
11//!
12//! # 責務
13//!
14//! - `id()` → この拠点の識別子
15//! - `kind()` → 拠点の物理的分類(コスト推定に使用)
16//! - `file_root()` → ファイルのベースパス
17//! - `scanner()` → この拠点のスキャン能力(LocationScanner)
18//! - `ensure()` → 到達確認 + 外部ツールの確保(rclone等)
19
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22
23use async_trait::async_trait;
24
25use crate::domain::location::LocationId;
26use crate::infra::error::InfraError;
27use crate::infra::location_scanner::LocationScanner;
28
29/// 拠点の物理的分類。
30///
31/// `SdkImplBuilder::build()` でルートコストを自動推定するために使用する。
32/// 2拠点間の転送コストは、双方の `LocationKind` の組み合わせで決まる:
33///
34/// | src → dest | コスト | 根拠 |
35/// |---|---|---|
36/// | Local → Remote | 1.0 | LAN/SSH、低レイテンシ |
37/// | Remote → Cloud | 2.0 | DC帯域、中速 |
38/// | Local → Cloud | 5.0 | 家庭回線アップロード、低速 |
39/// | Cloud → Remote | 2.0 | DC帯域、中速 |
40/// | Cloud → Local | 5.0 | 家庭回線ダウンロード |
41/// | Remote → Local | 1.0 | LAN/SSH |
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum LocationKind {
44    /// ローカルファイルシステム(開発マシン等)。
45    Local,
46    /// SSH経由リモートホスト(GPU Pod, NAS等)。データセンター帯域。
47    Remote,
48    /// クラウドストレージ(B2, S3等)。オブジェクトストア。
49    Cloud,
50}
51
52/// 拠点の多態抽象。
53///
54/// 各拠点は自分のスキャン方法を知っている:
55/// - Local: walkdir + ContentHasher
56/// - SSH: RemoteShell + batch_inspect
57/// - Cloud: StorageBackend.list() (metadata only)
58///
59/// `Location` trait 実装を `SdkImplBuilder::location()` に渡すことで、
60/// Scanner と Route の整合性が保証される。
61/// `kind()` は `SdkImplBuilder::build()` でルートコストの自動推定に使用される。
62#[async_trait]
63pub trait Location: Send + Sync {
64    /// この拠点の識別子。
65    fn id(&self) -> &LocationId;
66
67    /// 拠点の物理的分類。
68    ///
69    /// ルート間コスト推定に使用される。
70    fn kind(&self) -> LocationKind;
71
72    /// ファイルのベースパス。
73    ///
74    /// Local: `/Users/.../output`
75    /// Pod: `/workspace/comfyui/output`
76    /// Cloud: `vdsl/output`
77    fn file_root(&self) -> &Path;
78
79    /// この拠点のスキャナーを返す。
80    ///
81    /// 各実装が自分のスキャン方法に応じたLocationScannerを構築して返す。
82    fn scanner(&self) -> Arc<dyn LocationScanner>;
83
84    /// 拠点の到達可能性を検証し、必要な外部ツールを確保する。
85    ///
86    /// sync開始前に全Locationに対して呼ばれる。
87    /// - Local: file_rootの存在確認(なければ作成)
88    /// - SSH: SSH接続テスト
89    /// - Cloud: rcloneバイナリ確認 + バケット接続テスト
90    ///
91    /// 失敗時は早期エラーで、数分かかるscanを無駄にしない。
92    async fn ensure(&self) -> Result<(), InfraError>;
93}
94
95// =============================================================================
96// LocalLocation
97// =============================================================================
98
99use crate::infra::hasher::ContentHasher;
100use crate::infra::location_scanner::LocalScanner;
101
102/// ローカルファイルシステムの拠点。
103///
104/// walkdir + ContentHasher でスキャンする。
105pub struct LocalLocation {
106    id: LocationId,
107    root: PathBuf,
108    hasher: Arc<dyn ContentHasher>,
109}
110
111impl LocalLocation {
112    pub fn new(root: PathBuf, hasher: Arc<dyn ContentHasher>) -> Self {
113        Self {
114            id: LocationId::local(),
115            root,
116            hasher,
117        }
118    }
119}
120
121#[async_trait]
122impl Location for LocalLocation {
123    fn id(&self) -> &LocationId {
124        &self.id
125    }
126
127    fn kind(&self) -> LocationKind {
128        LocationKind::Local
129    }
130
131    fn file_root(&self) -> &Path {
132        &self.root
133    }
134
135    fn scanner(&self) -> Arc<dyn LocationScanner> {
136        Arc::new(LocalScanner::new(
137            self.id.clone(),
138            self.root.clone(),
139            self.hasher.clone(),
140        ))
141    }
142
143    async fn ensure(&self) -> Result<(), InfraError> {
144        if !self.root.exists() {
145            std::fs::create_dir_all(&self.root).map_err(|e| {
146                InfraError::Init(format!(
147                    "local file_root '{}' does not exist and could not be created: {e}",
148                    self.root.display()
149                ))
150            })?;
151        }
152        if !self.root.is_dir() {
153            return Err(InfraError::Init(format!(
154                "local file_root '{}' exists but is not a directory",
155                self.root.display()
156            )));
157        }
158        Ok(())
159    }
160}
161
162// =============================================================================
163// SshLocation
164// =============================================================================
165
166use crate::infra::location_scanner::SshScanner;
167use crate::infra::shell::RemoteShell;
168
169/// SSH経由リモートホストの拠点。
170///
171/// RemoteShell.batch_inspect() でスキャンする。
172pub struct SshLocation {
173    id: LocationId,
174    root: PathBuf,
175    shell: Arc<dyn RemoteShell>,
176}
177
178impl SshLocation {
179    pub fn new(id: LocationId, root: PathBuf, shell: Arc<dyn RemoteShell>) -> Self {
180        Self { id, root, shell }
181    }
182}
183
184#[async_trait]
185impl Location for SshLocation {
186    fn id(&self) -> &LocationId {
187        &self.id
188    }
189
190    fn kind(&self) -> LocationKind {
191        LocationKind::Remote
192    }
193
194    fn file_root(&self) -> &Path {
195        &self.root
196    }
197
198    fn scanner(&self) -> Arc<dyn LocationScanner> {
199        Arc::new(SshScanner::new(
200            self.id.clone(),
201            self.root.clone(),
202            self.shell.clone(),
203        ))
204    }
205
206    async fn ensure(&self) -> Result<(), InfraError> {
207        let output = self.shell.exec(&["echo", "pong"], Some(30)).await?;
208        if !output.success {
209            return Err(InfraError::Init(format!(
210                "SSH location '{}' unreachable (exit {}): {}",
211                self.id,
212                output.exit_code.unwrap_or(-1),
213                output.stderr.trim()
214            )));
215        }
216        Ok(())
217    }
218}
219
220// =============================================================================
221// CloudLocation
222// =============================================================================
223
224use crate::infra::backend::StorageBackend;
225use crate::infra::location_scanner::CloudScanner;
226
227/// Cloud storage の拠点。
228///
229/// StorageBackend.list() でメタデータのみ取得する。
230/// コンテンツハッシュはダウンロードが必要なため取得しない。
231pub struct CloudLocation {
232    id: LocationId,
233    root: PathBuf,
234    backend: Arc<dyn StorageBackend>,
235}
236
237impl CloudLocation {
238    pub fn new(id: LocationId, root: PathBuf, backend: Arc<dyn StorageBackend>) -> Self {
239        Self { id, root, backend }
240    }
241}
242
243#[async_trait]
244impl Location for CloudLocation {
245    fn id(&self) -> &LocationId {
246        &self.id
247    }
248
249    fn kind(&self) -> LocationKind {
250        LocationKind::Cloud
251    }
252
253    fn file_root(&self) -> &Path {
254        &self.root
255    }
256
257    fn scanner(&self) -> Arc<dyn LocationScanner> {
258        Arc::new(CloudScanner::new(
259            self.id.clone(),
260            self.root.clone(),
261            self.backend.clone(),
262        ))
263    }
264
265    async fn ensure(&self) -> Result<(), InfraError> {
266        self.backend.ensure().await.map_err(|e| {
267            InfraError::Init(format!("cloud location '{}' ensure failed: {e}", self.id))
268        })
269    }
270}