cli_text_reader_online/
lib.rs

1mod config;
2mod editor;
3mod progress;
4mod server;
5mod tutorial;
6
7use chrono::Utc;
8use editor::Editor;
9use server::{HyggClient, ReadingProgress};
10use std::collections::hash_map::DefaultHasher;
11use std::hash::{Hash, Hasher};
12use std::path::Path;
13use tokio::fs::read_to_string;
14use uuid::Uuid;
15
16/// Upload a local file to the server for syncing
17///
18/// This allows users to upload their files to the server so they can be
19/// accessed from other clients and their reading progress can be tracked
20/// online.
21///
22/// # Arguments
23/// * `local_file_path` - Path to the local file to upload
24/// * `user_id` - User identifier
25///
26/// # Returns
27/// * `Result<(), Box<dyn std::error::Error>>` - Result indicating success or
28///   error
29pub async fn upload_file_to_server(
30  local_file_path: String,
31  user_id: String,
32) -> Result<(), Box<dyn std::error::Error>> {
33  // Check if file exists
34  if !Path::new(&local_file_path).exists() {
35    return Err(format!("File not found: {local_file_path}").into());
36  }
37
38  // Read file content
39  let content = read_to_string(&local_file_path).await?.replace('\r', ""); // Normalize line endings
40
41  // Extract filename from path
42  let file_name = Path::new(&local_file_path)
43    .file_name()
44    .and_then(|name| name.to_str())
45    .ok_or_else(|| format!("Invalid file path: {local_file_path}"))?;
46
47  // Create client and upload file
48  let client = HyggClient::new(user_id.clone());
49  client.upload_file(file_name, &content).await?;
50
51  println!("Successfully uploaded {file_name} to the server");
52  Ok(())
53}
54
55pub async fn run_cli_text_reader(
56  file_path: String,
57  user_id: String,
58  col: usize,
59) -> Result<(), Box<dyn std::error::Error>> {
60  let client = HyggClient::new(user_id.clone());
61
62  // Get file content from server
63  let content = client.get_file_content(&file_path).await?;
64  let lines: Vec<String> = content.lines().map(String::from).collect();
65
66  // Create or get progress with file-based ID
67  // Generate consistent UUID based on file path
68  let mut hasher = DefaultHasher::new();
69  file_path.hash(&mut hasher);
70  let hash = hasher.finish();
71
72  // Create a reproducible UUID by using the hash as seed
73  // This ensures the same file always gets the same UUID
74  let hash_bytes = hash.to_be_bytes();
75  let uuid_bytes = [
76    hash_bytes[0],
77    hash_bytes[1],
78    hash_bytes[2],
79    hash_bytes[3],
80    hash_bytes[4],
81    hash_bytes[5],
82    hash_bytes[6],
83    hash_bytes[7],
84    hash_bytes[0],
85    hash_bytes[1],
86    hash_bytes[2],
87    hash_bytes[3],
88    hash_bytes[4],
89    hash_bytes[5],
90    hash_bytes[6],
91    hash_bytes[7],
92  ];
93
94  let file_uuid = Uuid::from_bytes(uuid_bytes);
95  println!("Using file-based UUID: {file_uuid}");
96
97  let mut progress = ReadingProgress {
98    id: file_uuid,
99    file_path,
100    position: 0,
101    user_id: user_id.clone(),
102    last_accessed: Utc::now(),
103    lock_holder: None,
104    lock_expiry: None,
105  };
106
107  // Try to acquire lock - if it fails, open in read-only mode
108  let read_only_mode = match client.acquire_lock(&progress).await {
109    Ok(locked_progress) => {
110      progress = locked_progress;
111      false // We have the lock, so not read-only
112    }
113    Err(e) => {
114      // Check if it's a lock error (someone else has the lock)
115      if e.to_string().contains("locked by") {
116        println!(
117          "\nOpening in READ-ONLY mode because file is locked by another user."
118        );
119        println!("Close and reopen to attempt to acquire the lock.\n");
120
121        // Try to get the current progress to display at the right position
122        match client.get_progress(&progress.id.to_string()).await {
123          Ok(current_progress) => {
124            progress = current_progress;
125            true // Read-only mode
126          }
127          Err(_) => {
128            // If we can't get progress, still open read-only at position 0
129            true // Read-only mode
130          }
131        }
132      } else {
133        // For non-lock errors, propagate the error
134        return Err(Box::new(std::io::Error::other(e.to_string())));
135      }
136    }
137  };
138
139  // Create editor with progress tracking
140  let mut editor = Editor::new(lines, col);
141  editor.set_position(progress.position);
142
143  let result = if read_only_mode {
144    editor.set_read_only(true);
145    // Run editor without progress updates
146    editor.run()
147  } else {
148    // Run editor with progress tracking
149    let client_clone = client.clone();
150    let progress_clone = progress.clone();
151
152    editor.run_with_progress(move |pos| {
153      let mut progress_update = progress_clone.clone();
154      progress_update.position = pos;
155      let client = client_clone.clone();
156      tokio::spawn(async move {
157        if let Err(e) = client.update_progress(&progress_update).await {
158          eprintln!("Failed to update progress: {e}");
159        }
160      });
161    })
162  };
163
164  // Release lock if we had one (not read-only mode)
165  if !read_only_mode {
166    match client.release_lock(&progress).await {
167      Ok(_) => println!(
168        "\nLock released successfully. Other users can now edit this file."
169      ),
170      Err(e) => eprintln!("\nFailed to release lock: {e}"),
171    }
172  }
173
174  result
175}