1use std::path::Path;
19use std::path::PathBuf;
20use std::sync::Arc;
21
22use log::debug;
23use openssh::KnownHosts;
24
25use super::SFTP_SCHEME;
26use super::config::SftpConfig;
27use super::core::SftpCore;
28use super::core::is_sftp_protocol_error;
29use super::core::parse_sftp_error;
30use super::core::to_metadata;
31use super::deleter::SftpDeleter;
32use super::reader::*;
33use opendal_core::raw::*;
34use opendal_core::*;
35
36#[doc = include_str!("docs.md")]
46#[derive(Debug, Default)]
47pub struct SftpBuilder {
48 pub(super) config: SftpConfig,
49}
50
51impl SftpBuilder {
52 pub fn endpoint(mut self, endpoint: &str) -> Self {
55 self.config.endpoint = if endpoint.is_empty() {
56 None
57 } else {
58 Some(endpoint.to_string())
59 };
60
61 self
62 }
63
64 pub fn root(mut self, root: &str) -> Self {
67 self.config.root = if root.is_empty() {
68 None
69 } else {
70 Some(root.to_string())
71 };
72
73 self
74 }
75
76 pub fn user(mut self, user: &str) -> Self {
78 self.config.user = if user.is_empty() {
79 None
80 } else {
81 Some(user.to_string())
82 };
83
84 self
85 }
86
87 pub fn key(mut self, key: &str) -> Self {
89 self.config.key = if key.is_empty() {
90 None
91 } else {
92 Some(key.to_string())
93 };
94
95 self
96 }
97
98 pub fn known_hosts_strategy(mut self, strategy: &str) -> Self {
104 self.config.known_hosts_strategy = if strategy.is_empty() {
105 None
106 } else {
107 Some(strategy.to_string())
108 };
109
110 self
111 }
112
113 #[deprecated(
115 since = "0.57.0",
116 note = "SFTP copy capability is enabled by default and this option is no longer needed."
117 )]
118 pub fn enable_copy(self, _enable_copy: bool) -> Self {
119 self
120 }
121}
122
123impl Builder for SftpBuilder {
124 type Config = SftpConfig;
125
126 fn build(self) -> Result<impl Service> {
127 debug!("sftp backend build started: {:?}", self);
128 let endpoint = match self.config.endpoint.clone() {
129 Some(v) => v,
130 None => return Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is empty")),
131 };
132
133 let user = self.config.user.clone();
134
135 let root = self
136 .config
137 .root
138 .clone()
139 .map(|r| normalize_root(r.as_str()))
140 .unwrap_or_default();
141
142 let known_hosts_strategy = match &self.config.known_hosts_strategy {
143 Some(v) => {
144 let v = v.to_lowercase();
145 if v == "strict" {
146 KnownHosts::Strict
147 } else if v == "accept" {
148 KnownHosts::Accept
149 } else if v == "add" {
150 KnownHosts::Add
151 } else {
152 return Err(Error::new(
153 ErrorKind::ConfigInvalid,
154 format!("unknown known_hosts strategy: {v}").as_str(),
155 ));
156 }
157 }
158 None => KnownHosts::Strict,
159 };
160
161 let info = ServiceInfo::new(SFTP_SCHEME, root.as_str(), "");
162 let capability = Capability {
163 stat: true,
164
165 read: true,
166
167 write: true,
168 write_can_multi: true,
169 write_with_if_not_exists: true,
170
171 create_dir: true,
172 delete: true,
173
174 list: true,
175 list_with_limit: true,
176
177 copy: true,
178 rename: true,
179
180 shared: true,
181
182 ..Default::default()
183 };
184
185 let core = Arc::new(SftpCore::new(
186 info,
187 capability,
188 endpoint,
189 root,
190 user,
191 self.config.key.clone(),
192 known_hosts_strategy,
193 ));
194
195 debug!("sftp backend finished: {:?}", self);
196 Ok(SftpBackend { core })
197 }
198}
199
200#[derive(Clone, Debug)]
201pub struct SftpBackend {
202 pub core: Arc<SftpCore>,
203}
204
205impl Service for SftpBackend {
206 type Reader = oio::StreamReader<SftpReader>;
207 type Writer = SftpLazyWriter;
208 type Lister = SftpLazyLister;
209 type Deleter = oio::OneShotDeleter<SftpDeleter>;
210 type Copier = oio::OneShotCopier;
211
212 fn info(&self) -> ServiceInfo {
213 self.core.info.clone()
214 }
215
216 fn capability(&self) -> Capability {
217 self.core.capability
218 }
219
220 async fn create_dir(
221 &self,
222 _ctx: &OperationContext,
223 path: &str,
224 _: OpCreateDir,
225 ) -> Result<RpCreateDir> {
226 let client = self.core.connect().await?;
227 let mut fs = client.fs();
228 fs.set_cwd(&self.core.root);
229
230 let paths = Path::new(&path).components();
231 let mut current = PathBuf::from(&self.core.root);
232 for p in paths {
233 current = current.join(p);
234 let res = fs.create_dir(p).await;
235
236 if let Err(e) = res {
237 if !is_sftp_protocol_error(&e) {
239 return Err(parse_sftp_error(e));
240 }
241 }
242 fs.set_cwd(¤t);
243 }
244
245 Ok(RpCreateDir::default())
246 }
247
248 async fn stat(&self, _ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
249 let client = self.core.connect().await?;
250 let mut fs = client.fs();
251 fs.set_cwd(&self.core.root);
252
253 let meta: Metadata = to_metadata(fs.metadata(path).await.map_err(parse_sftp_error)?);
254
255 Ok(RpStat::new(meta))
256 }
257 fn read(&self, _ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
258 let output: oio::StreamReader<SftpReader> = {
259 Ok(oio::StreamReader::new(SftpReader::new(
260 self.clone(),
261 path,
262 args,
263 )))
264 }?;
265
266 Ok(output)
267 }
268
269 fn write(&self, ctx: &OperationContext, path: &str, op: OpWrite) -> Result<Self::Writer> {
270 Ok(SftpLazyWriter::new(self.clone(), ctx.clone(), path, op))
271 }
272
273 fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
274 let output: oio::OneShotDeleter<SftpDeleter> = {
275 Ok(oio::OneShotDeleter::new(SftpDeleter::new(
276 self.core.clone(),
277 )))
278 }?;
279
280 Ok(output)
281 }
282
283 fn list(&self, _ctx: &OperationContext, path: &str, _: OpList) -> Result<Self::Lister> {
284 Ok(SftpLazyLister::new(self.clone(), path))
285 }
286
287 fn copy(
288 &self,
289 ctx: &OperationContext,
290 from: &str,
291 to: &str,
292 _: OpCopy,
293 _opts: OpCopier,
294 ) -> Result<Self::Copier> {
295 let backend = self.clone();
296 let ctx = ctx.clone();
297 let from = from.to_string();
298 let to = to.to_string();
299 Ok(oio::OneShotCopier::new(async move {
300 let client = backend.core.connect().await?;
301
302 let mut fs = client.fs();
303 fs.set_cwd(&backend.core.root);
304
305 if let Some((dir, _)) = to.rsplit_once('/') {
306 backend
307 .create_dir(&ctx, dir, OpCreateDir::default())
308 .await?;
309 }
310
311 let src = fs.canonicalize(&from).await.map_err(parse_sftp_error)?;
312 let dst = fs.canonicalize(&to).await.map_err(parse_sftp_error)?;
313 let mut src_file = client.open(&src).await.map_err(parse_sftp_error)?;
314 let mut dst_file = client.create(dst).await.map_err(parse_sftp_error)?;
315
316 src_file
317 .copy_all_to(&mut dst_file)
318 .await
319 .map_err(parse_sftp_error)?;
320
321 Ok(Metadata::default())
322 }))
323 }
324
325 async fn rename(
326 &self,
327 ctx: &OperationContext,
328 from: &str,
329 to: &str,
330 _: OpRename,
331 ) -> Result<RpRename> {
332 let client = self.core.connect().await?;
333
334 let mut fs = client.fs();
335 fs.set_cwd(&self.core.root);
336
337 if let Some((dir, _)) = to.rsplit_once('/') {
338 self.create_dir(ctx, dir, OpCreateDir::default()).await?;
339 }
340 fs.rename(from, to).await.map_err(parse_sftp_error)?;
341
342 Ok(RpRename::default())
343 }
344
345 async fn presign(
346 &self,
347 _ctx: &OperationContext,
348 _path: &str,
349 _args: OpPresign,
350 ) -> Result<RpPresign> {
351 Err(Error::new(
352 ErrorKind::Unsupported,
353 "operation is not supported",
354 ))
355 }
356}