makepad_file_server/
file_server.rs

1use {
2    crate::{
3        makepad_file_protocol::{
4            DirectoryEntry,
5            FileNodeData,
6            FileTreeData,
7            FileError,
8            FileNotification,
9            FileRequest,
10            FileResponse,
11        },
12    },
13    std::{
14        cmp::Ordering,
15        fmt,
16        fs,
17        path::{Path, PathBuf},
18        sync::{Arc, RwLock},
19    },
20};
21
22pub struct FileServer {
23    // The id for the next connection
24    next_connection_id: usize,
25    // State that is shared between every connection
26    shared: Arc<RwLock<Shared >>,
27}
28
29impl FileServer {
30    /// Creates a new collab server rooted at the given path.
31    pub fn new<P: Into<PathBuf >> (root_path: P) -> FileServer {
32        FileServer {
33            next_connection_id: 0,
34            shared: Arc::new(RwLock::new(Shared {
35                root_path: root_path.into(),
36            })),
37        }
38    }
39    
40    /// Creates a new connection to this collab server, and returns a handle for the connection.
41    ///
42    /// The given `notification_sender` is called whenever the server wants to send a notification
43    /// for this connection. The embedder is responsible for sending the notification.
44    pub fn connect(&mut self, notification_sender: Box<dyn NotificationSender>) -> FileServerConnection {
45        let connection_id = ConnectionId(self.next_connection_id);
46        self.next_connection_id += 1;
47        FileServerConnection {
48            _connection_id:connection_id,
49            shared: self.shared.clone(),
50            _notification_sender: notification_sender
51        }
52    }
53}
54
55/// A connection to a collab server.
56pub struct FileServerConnection {
57    // The id for this connection.
58    _connection_id: ConnectionId,
59    // State is shared between every connection.
60    shared: Arc<RwLock<Shared >>,
61    // Used to send notifications for this connection.
62    _notification_sender: Box<dyn NotificationSender>,
63}
64
65impl FileServerConnection {
66    /// Handles the given `request` for this connection, and returns the corresponding response.
67    ///
68    /// The embedder is responsible for receiving requests, calling this method to handle them, and
69    /// sending back the response.
70    pub fn handle_request(&self, request: FileRequest) -> FileResponse {
71        
72        
73        match request {
74            FileRequest::LoadFileTree {with_data} => FileResponse::LoadFileTree(self.load_file_tree(with_data)),
75            FileRequest::OpenFile(path,id) => FileResponse::OpenFile(self.open_file(path, id)),
76            FileRequest::SaveFile(path, delta, id) => FileResponse::SaveFile(self.save_file(path, delta, id)),
77        }
78    }
79    
80    // Handles a `LoadFileTree` request.
81    fn load_file_tree(&self, with_data: bool) -> Result<FileTreeData, FileError> {
82        // A recursive helper function for traversing the entries of a directory and creating the
83        // data structures that describe them.
84        fn get_directory_entries(path: &Path, with_data: bool) -> Result<Vec<DirectoryEntry>, FileError> {
85            let mut entries = Vec::new();
86            for entry in fs::read_dir(path).map_err( | error | FileError::Unknown(error.to_string())) ? {
87                // We can't get the entry for some unknown reason. Raise an error.
88                let entry = entry.map_err( | error | FileError::Unknown(error.to_string())) ?;
89                // Get the path for the entry.
90                let entry_path = entry.path();
91                // Get the file name for the entry.
92                let name = entry.file_name();
93                if let Ok(name_string) = name.into_string() {
94                    if entry_path.is_dir() && name_string == "target"
95                        || name_string.starts_with('.') {
96                        // Skip over directories called "target". This is sort of a hack. The reason
97                        // it's here is that the "target" directory for Rust projects is huge, and
98                        // our current implementation of the file tree widget is not yet fast enough
99                        // to display vast numbers of nodes. We paper over this by pretending the
100                        // "target" directory does not exist.
101                        continue;
102                    }
103                }
104                else {
105                    // Skip over entries with a non UTF-8 file name.
106                    continue;
107                }
108                // Create a `DirectoryEntry` for this entry and add it to the list of entries.
109                entries.push(DirectoryEntry {
110                    name: entry.file_name().to_string_lossy().to_string(),
111                    node: if entry_path.is_dir() {
112                        // If this entry is a subdirectory, recursively create `DirectoryEntry`'s
113                        // for its entries as well.
114                        FileNodeData::Directory {
115                            entries: get_directory_entries(&entry_path, with_data) ?,
116                        }
117                    } else if entry_path.is_file() {
118                        if with_data {
119                            let bytes: Vec<u8> = fs::read(&entry_path).map_err(
120                                | error | FileError::Unknown(error.to_string())
121                            ) ?;
122                            FileNodeData::File {data: Some(bytes)}
123                        }
124                        else {
125                            FileNodeData::File {data: None}
126                        }
127                    }
128                    else {
129                        // If this entry is neither a directory or a file, skip it. This ignores
130                        // things such as symlinks, for which we are not yet sure how we want to
131                        // handle them.
132                        continue
133                    },
134                });
135            }
136            
137            // Sort all the entries by name, directories first, and files second.
138            entries.sort_by( | entry_0, entry_1 | {
139                match &entry_0.node {
140                    FileNodeData::Directory {..} => match &entry_1.node {
141                        FileNodeData::Directory {..} => entry_0.name.cmp(&entry_1.name),
142                        FileNodeData::File {..} => Ordering::Less
143                    }
144                    FileNodeData::File {..} => match &entry_1.node {
145                        FileNodeData::Directory {..} => Ordering::Greater,
146                        FileNodeData::File {..} => entry_0.name.cmp(&entry_1.name)
147                    }
148                }
149            });
150            Ok(entries)
151        }
152        
153        let root_path = self.shared.read().unwrap().root_path.clone();
154        
155        let root = FileNodeData::Directory {
156            entries: get_directory_entries(&root_path, with_data) ?,
157        };
158        Ok(FileTreeData {root_path: "".into(), root})
159    }
160    
161    fn make_full_path(&self, child_path:&String)->PathBuf{
162        let mut path = self.shared.read().unwrap().root_path.clone();
163        path.push(child_path);
164        path
165    }
166    
167    // Handles an `OpenFile` request.
168    fn open_file(&self, child_path: String, id:u64) -> Result<(String, String, u64), FileError> {
169        let path = self.make_full_path(&child_path);
170        
171        let bytes = fs::read(&path).map_err(
172            | error | FileError::Unknown(error.to_string())
173        ) ?;
174        // Converts the file contents to a `Text`. This is necessarily a lossy conversion
175        // because `Text` assumes everything is UTF-8 encoded, and this isn't always the
176        // case for files on disk (is this a problem?)
177        /*let text: Text = Text::from_lines(String::from_utf8_lossy(&bytes)
178            .lines()
179            .map( | line | line.chars().collect::<Vec<_ >> ())
180            .collect::<Vec<_ >>());*/
181        
182        let text = String::from_utf8_lossy(&bytes);
183        Ok((child_path, text.to_string(), id))
184    }
185    
186    // Handles an `ApplyDelta` request.
187    fn save_file(
188        &self,
189        child_path: String,
190        new_content: String,
191        id: u64
192    ) -> Result<(String, String, String, u64), FileError> {
193        let path = self.make_full_path(&child_path);
194        
195        let old_content = String::from_utf8_lossy(&fs::read(&path).map_err(
196            | error | FileError::Unknown(error.to_string())
197        ) ?).to_string();
198
199        fs::write(&path, &new_content).map_err(
200            | error | FileError::Unknown(error.to_string())
201        ) ?;
202        
203        Ok((child_path, old_content, new_content, id))
204    }
205}
206
207/// A trait for sending notifications over a connection.
208pub trait NotificationSender: Send {
209    /// This method is necessary to create clones of boxed trait objects.
210    fn box_clone(&self) -> Box<dyn NotificationSender>;
211    
212    /// This method is called to send a notification over the corresponding connection.
213    fn send_notification(&self, notification: FileNotification);
214}
215
216impl<F: Clone + Fn(FileNotification) + Send + 'static> NotificationSender for F {
217    fn box_clone(&self) -> Box<dyn NotificationSender> {
218        Box::new(self.clone())
219    }
220    
221    fn send_notification(&self, notification: FileNotification) {
222        self (notification)
223    }
224}
225
226impl Clone for Box<dyn NotificationSender> {
227    fn clone(&self) -> Self {
228        self.box_clone()
229    }
230}
231
232impl fmt::Debug for dyn NotificationSender {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        write!(f, "NotificationSender")
235    }
236}
237
238// State that is shared between every connection.
239#[derive(Debug)]
240struct Shared {
241    root_path: PathBuf,
242}
243
244/// An identifier for a connection.
245#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
246struct ConnectionId(usize);
247