1use std::borrow::Cow;
2
3use anyhow::{anyhow, Context, Result};
4use opendal::{services, Entry, EntryMode, Operator};
5use serde::{Deserialize, Serialize};
6use serde_yml::{from_str, to_string as to_yml_string};
7use tokio::{fs::File, io::AsyncWriteExt};
8
9use crate::common::{path_to_config_folder, path_to_string, tilde, CONFIG_FOLDER};
10use crate::io::{CowStr, DrawMenu};
11use crate::modes::{human_size, FileInfo};
12use crate::{impl_content, impl_selectable, log_info, log_line};
13
14#[derive(Serialize, Deserialize, Debug)]
19pub struct GoogleDriveConfig {
20 drive_name: String,
21 root_folder: String,
22 refresh_token: String,
23 client_id: String,
24 client_secret: String,
25}
26
27impl GoogleDriveConfig {
28 pub fn new(
29 drive_name: String,
30 root_folder: String,
31 refresh_token: String,
32 client_id: String,
33 client_secret: String,
34 ) -> Self {
35 Self {
36 drive_name,
37 root_folder,
38 refresh_token,
39 client_id,
40 client_secret,
41 }
42 }
43
44 pub fn serialize(&self) -> Result<String> {
45 Ok(to_yml_string(self)?)
46 }
47
48 fn build_token_filename(config_name: &str) -> String {
49 let token_base_path = tilde(CONFIG_FOLDER);
50 format!("{token_base_path}/token_{config_name}.yaml")
51 }
52
53 async fn from_config(config_name: &str) -> Result<Self> {
55 let config_filename = Self::build_token_filename(config_name);
56 let token_data = tokio::fs::read_to_string(&config_filename).await?;
57 let google_drive_token: Self = from_str(&token_data)?;
58 Ok(google_drive_token)
59 }
60
61 async fn build_operator(&self) -> Result<Operator> {
63 let builder = services::Gdrive::default()
64 .refresh_token(&self.refresh_token)
65 .client_id(&self.client_id)
66 .client_secret(&self.client_secret)
67 .root(&self.root_folder);
68
69 let op = Operator::new(builder)?.finish();
70 Ok(op)
71 }
72}
73
74#[tokio::main]
76pub async fn google_drive(token_file: &str) -> Result<OpendalContainer> {
77 let google_drive_config = GoogleDriveConfig::from_config(token_file).await?;
78 log_info!("found google drive config {token_file}");
79 let op = google_drive_config.build_operator().await?;
80 log_info!("created operator");
81
82 let entries = match op.list(&google_drive_config.root_folder).await {
85 Ok(entries) => entries,
86 Err(err) => {
87 log_info!("Error: {err:?}");
88 return Err(anyhow!("error: {err:?}"));
89 }
90 };
91 log_info!("listed entries");
92
93 let opendal_container = OpendalContainer::new(
95 op,
96 OpendalKind::GoogleDrive,
97 &google_drive_config.drive_name,
98 &google_drive_config.root_folder,
99 entries,
100 );
101
102 Ok(opendal_container)
103}
104
105#[derive(Default)]
107pub enum OpendalKind {
108 #[default]
109 Empty,
110 GoogleDrive,
111}
112
113impl OpendalKind {
114 fn repr(&self) -> &'static str {
115 match self {
116 Self::Empty => "empty",
117 Self::GoogleDrive => "Google Drive",
118 }
119 }
120}
121
122pub fn get_cloud_token_names() -> Result<Vec<String>> {
123 Ok(std::fs::read_dir(path_to_config_folder()?)?
124 .filter_map(|e| e.ok())
125 .filter(|e| e.path().is_file())
126 .map(|e| e.file_name().to_string_lossy().to_string())
127 .filter(|filename| filename.starts_with("token_") && filename.ends_with(".yaml"))
128 .map(|filename| filename.replace("token_", "").replace(".yaml", ""))
129 .collect())
130}
131
132pub trait ModeFormat {
134 fn mode_fmt(&self) -> &'static str;
135}
136
137impl ModeFormat for Entry {
138 fn mode_fmt(&self) -> &'static str {
139 match self.metadata().mode() {
140 EntryMode::Unknown => "? ",
141 EntryMode::DIR => "d ",
142 EntryMode::FILE => ". ",
143 }
144 }
145}
146
147#[derive(Default)]
151pub struct OpendalContainer {
152 op: Option<Operator>,
154 kind: OpendalKind,
157 name: String,
159 path: std::path::PathBuf,
161 root: std::path::PathBuf,
163 pub index: usize,
165 pub content: Vec<Entry>,
167 pub metadata_repr: Option<(usize, String)>,
171}
172
173impl OpendalContainer {
174 fn new(
175 op: Operator,
176 kind: OpendalKind,
177 drive_name: &str,
178 root_path: &str,
179 content: Vec<Entry>,
180 ) -> Self {
181 Self {
182 op: Some(op),
183 name: format!("{kind_format}/{drive_name}", kind_format = kind.repr()),
184 path: std::path::PathBuf::from(root_path),
185 root: std::path::PathBuf::from(root_path),
186 kind,
187 index: 0,
188 content,
189 metadata_repr: None,
190 }
191 }
192
193 fn selected_filepath(&self) -> Option<String> {
194 Some(format!(
195 "{path}{sep}{filename}",
196 path = self.path.display(),
197 sep = if self.path == self.root { "" } else { "/" },
198 filename = self.selected_filename()?,
199 ))
200 }
201
202 #[tokio::main]
204 pub async fn update_metadata(&mut self) -> Result<()> {
205 let Some(op) = &self.op else {
206 return Ok(());
207 };
208 let Some(filename) = self.selected_filename() else {
209 return Ok(());
210 };
211 let metadata = op
212 .stat_with(&self.selected_filepath().context("No selected file")?)
213 .await?;
214 let last_modified = match metadata.last_modified() {
215 Some(dt) => &dt.format("%Y/%m/%d %H:%M:%S").to_string(),
216 None => "",
217 };
218 let size = human_size(metadata.content_length());
219 let metadata_repr = format!("{size} {last_modified} {filename} ");
220 self.metadata_repr = Some((self.index, metadata_repr));
221
222 Ok(())
223 }
224
225 pub fn is_set(&self) -> bool {
227 self.op.is_some()
228 }
229
230 fn cloud_build_dest_filename(&self, local_file: &FileInfo) -> String {
231 let filename = local_file.filename.as_ref();
232 let mut dest_path = self.path.clone();
233 dest_path.push(filename);
234 path_to_string(&dest_path)
235 }
236
237 #[tokio::main]
239 pub async fn upload(&self, local_file: &FileInfo) -> Result<()> {
240 let Some(op) = &self.op else {
241 return Ok(());
242 };
243 let dest_path_str = self.cloud_build_dest_filename(local_file);
244 let bytes = tokio::fs::read(&local_file.path).await?;
245 op.write(&dest_path_str, bytes).await?;
246 log_line!(
247 "Uploaded {filename} to {path}",
248 filename = local_file.filename,
249 path = self.path.display()
250 );
251 Ok(())
252 }
253
254 fn selected_filename(&self) -> Option<&str> {
255 self.selected()?.path().split('/').last()
256 }
257
258 fn create_downloaded_path(&self, dest: &std::path::Path) -> Option<std::path::PathBuf> {
259 let distant_filename = self.selected_filename()?;
260 let mut dest = dest.to_path_buf();
261 dest.push(distant_filename);
262 if dest.exists() {
263 log_info!(
264 "Local file {dest} already exists. Can't download here",
265 dest = dest.display()
266 );
267 log_line!("Local file {dest} already exists. Choose another path or rename the existing file first.", dest=dest.display());
268 None
269 } else {
270 Some(dest)
271 }
272 }
273
274 #[tokio::main]
280 pub async fn download(&self, dest: &std::path::Path) -> Result<()> {
281 let Some(op) = &self.op else {
282 return Ok(());
283 };
284 let Some(selected) = self.selected() else {
285 return Ok(());
286 };
287 let distant_filepath = selected.path();
288 let Some(dest_full_path) = self.create_downloaded_path(dest) else {
289 return Ok(());
290 };
291 let buf = op.read(distant_filepath).await?;
292 let mut file = File::create(&dest_full_path).await?;
293 file.write_all(&buf.to_bytes()).await?;
294 log_info!(
295 "Downloaded {distant_filepath} to local file {path}",
296 path = dest_full_path.display(),
297 );
298 Ok(())
299 }
300
301 #[tokio::main]
303 pub async fn create_newdir(&mut self, dirname: String) -> Result<()> {
304 let current_path = &self.path;
305 let Some(op) = &self.op else {
306 return Err(anyhow!("Cloud container has no operator"));
307 };
308 let fp = current_path.join(dirname);
309 let mut fullpath = path_to_string(&fp);
310 if !fullpath.ends_with('/') {
311 fullpath.push('/');
312 }
313 op.create_dir(&fullpath).await?;
314 Ok(())
315 }
316
317 pub fn disconnect(&mut self) {
319 let desc = self.name.to_owned();
320 self.op = None;
321 self.kind = OpendalKind::Empty;
322 self.name = "empty".to_owned();
323 self.path = std::path::PathBuf::from("");
324 self.root = std::path::PathBuf::from("");
325 self.index = 0;
326 self.content = vec![];
327 log_info!("Disconnected from {desc}");
328 }
329
330 #[tokio::main]
333 pub async fn delete(&mut self) -> Result<()> {
334 let Some(op) = &self.op else {
335 return Ok(());
336 };
337 let Some(entry) = self.selected() else {
338 return Ok(());
339 };
340 let file_to_delete = entry.path();
341 op.delete(file_to_delete).await?;
342 log_info!("Deleted {file_to_delete}");
343 log_line!("Deleted {file_to_delete}");
344 Ok(())
345 }
346
347 async fn update_path(&mut self, path: &str) -> Result<()> {
348 if let Some(op) = &self.op {
349 self.content = op.list(path).await?;
350 self.path = std::path::PathBuf::from(path);
351 self.index = 0;
352 self.metadata_repr = None;
353 };
354 Ok(())
355 }
356
357 #[tokio::main]
363 pub async fn enter_selected(&mut self) -> Result<()> {
364 let path = self.selected().context("no path")?.path().to_owned();
365 self.update_path(&path).await
366 }
367
368 fn ensure_index_in_bounds(&mut self) {
369 self.index = std::cmp::min(self.content.len().saturating_sub(1), self.index)
370 }
371
372 #[tokio::main]
375 pub async fn refresh_current(&mut self) -> Result<()> {
376 let old_index = self.index;
377 self.update_path(&path_to_string(&self.path)).await?;
378 self.index = old_index;
379 self.ensure_index_in_bounds();
380 Ok(())
381 }
382
383 #[tokio::main]
385 pub async fn move_to_parent(&mut self) -> Result<()> {
386 if self.op.is_some() {
387 if self.path == self.root {
388 return Ok(());
389 };
390 if let Some(parent) = self.path.to_owned().parent() {
391 self.update_path(&path_to_string(&parent)).await?;
392 }
393 }
394 Ok(())
395 }
396
397 pub fn desc(&self) -> String {
399 format!(
400 "{desc}{sep}{path}",
401 desc = self.name,
402 sep = if self.path == self.root { "" } else { "/" },
403 path = self.path.display()
404 )
405 }
406}
407
408impl_selectable!(OpendalContainer);
409impl_content!(OpendalContainer, Entry);
410
411impl CowStr for Entry {
412 fn cow_str(&self) -> Cow<str> {
413 format!("{mode} {path}", mode = self.mode_fmt(), path = self.path()).into()
414 }
415}
416
417impl DrawMenu<Entry> for OpendalContainer {}