Skip to content

Commit fb93910

Browse files
committed
feat: Implement autosave functionality and room management
- Added autosave snapshot creation and updating in LocalStorage and ServerStorage. - Introduced RoomRegistry interface for managing active rooms. - Implemented room deletion with confirmation in the API. - Updated room settings to use a 60-second autosave interval by default. - Enhanced the SQLite document store to support room activity tracking and deletion. - Added tests for autosave snapshot handling and room deletion. - Modified WebSocket handlers to touch room activity on user interactions.
1 parent fdf2eed commit fb93910

File tree

21 files changed

+1613
-192
lines changed

21 files changed

+1613
-192
lines changed

excalidraw-app/src-tauri/src/commands.rs

Lines changed: 125 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
1-
use crate::db::{Drawing, Snapshot, RoomSettings, DB};
1+
use crate::db::{Drawing, RoomSettings, Snapshot, DB};
22
use std::time::{SystemTime, UNIX_EPOCH};
33

4+
const AUTOSAVE_CREATED_BY: &str = "__autosave__";
5+
const AUTOSAVE_DEFAULT_NAME: &str = "Latest autosave snapshot";
6+
const AUTOSAVE_DEFAULT_DESCRIPTION: &str = "Automatically saved by Excalidraw";
7+
48
#[tauri::command]
59
pub fn save_drawing(name: String, data: String) -> Result<String, String> {
610
let timestamp = SystemTime::now()
711
.duration_since(UNIX_EPOCH)
812
.unwrap()
913
.as_secs() as i64;
10-
14+
1115
let id = uuid::Uuid::new_v4().to_string();
12-
16+
1317
let conn = DB.lock().map_err(|e| e.to_string())?;
14-
18+
1519
conn.execute(
1620
"INSERT INTO drawings (id, name, data, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
1721
rusqlite::params![&id, &name, &data, timestamp, timestamp],
1822
)
1923
.map_err(|e| e.to_string())?;
20-
24+
2125
Ok(id)
2226
}
2327

@@ -27,22 +31,22 @@ pub fn update_drawing(id: String, name: String, data: String) -> Result<(), Stri
2731
.duration_since(UNIX_EPOCH)
2832
.unwrap()
2933
.as_secs() as i64;
30-
34+
3135
let conn = DB.lock().map_err(|e| e.to_string())?;
32-
36+
3337
conn.execute(
3438
"UPDATE drawings SET name = ?1, data = ?2, updated_at = ?3 WHERE id = ?4",
3539
rusqlite::params![&name, &data, timestamp, &id],
3640
)
3741
.map_err(|e| e.to_string())?;
38-
42+
3943
Ok(())
4044
}
4145

4246
#[tauri::command]
4347
pub fn load_drawing(id: String) -> Result<Drawing, String> {
4448
let conn = DB.lock().map_err(|e| e.to_string())?;
45-
49+
4650
let drawing = conn
4751
.query_row(
4852
"SELECT id, name, data, created_at, updated_at FROM drawings WHERE id = ?1",
@@ -58,18 +62,20 @@ pub fn load_drawing(id: String) -> Result<Drawing, String> {
5862
},
5963
)
6064
.map_err(|e| e.to_string())?;
61-
65+
6266
Ok(drawing)
6367
}
6468

6569
#[tauri::command]
6670
pub fn list_drawings() -> Result<Vec<Drawing>, String> {
6771
let conn = DB.lock().map_err(|e| e.to_string())?;
68-
72+
6973
let mut stmt = conn
70-
.prepare("SELECT id, name, data, created_at, updated_at FROM drawings ORDER BY updated_at DESC")
74+
.prepare(
75+
"SELECT id, name, data, created_at, updated_at FROM drawings ORDER BY updated_at DESC",
76+
)
7177
.map_err(|e| e.to_string())?;
72-
78+
7379
let drawings = stmt
7480
.query_map([], |row| {
7581
Ok(Drawing {
@@ -83,17 +89,17 @@ pub fn list_drawings() -> Result<Vec<Drawing>, String> {
8389
.map_err(|e| e.to_string())?
8490
.collect::<Result<Vec<_>, _>>()
8591
.map_err(|e| e.to_string())?;
86-
92+
8793
Ok(drawings)
8894
}
8995

9096
#[tauri::command]
9197
pub fn delete_drawing(id: String) -> Result<(), String> {
9298
let conn = DB.lock().map_err(|e| e.to_string())?;
93-
99+
94100
conn.execute("DELETE FROM drawings WHERE id = ?1", rusqlite::params![&id])
95101
.map_err(|e| e.to_string())?;
96-
102+
97103
Ok(())
98104
}
99105

@@ -112,14 +118,14 @@ pub fn save_snapshot(
112118
.duration_since(UNIX_EPOCH)
113119
.unwrap()
114120
.as_secs() as i64;
115-
121+
116122
let id = uuid::Uuid::new_v4().to_string();
117-
123+
118124
let conn = DB.lock().map_err(|e| e.to_string())?;
119-
125+
120126
// Get room settings to check max snapshots
121127
let settings = get_room_settings_internal(&conn, &room_id)?;
122-
128+
123129
// Count existing snapshots
124130
let count: i32 = conn
125131
.query_row(
@@ -128,7 +134,7 @@ pub fn save_snapshot(
128134
|row| row.get(0),
129135
)
130136
.map_err(|e| e.to_string())?;
131-
137+
132138
// If at limit, delete oldest snapshot
133139
if count >= settings.max_snapshots {
134140
conn.execute(
@@ -137,25 +143,25 @@ pub fn save_snapshot(
137143
)
138144
.map_err(|e| e.to_string())?;
139145
}
140-
146+
141147
// Insert new snapshot
142148
conn.execute(
143149
"INSERT INTO snapshots (id, room_id, name, description, thumbnail, created_by, created_at, data) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
144150
rusqlite::params![&id, &room_id, &name, &description, &thumbnail, &created_by, timestamp, &data],
145151
)
146152
.map_err(|e| e.to_string())?;
147-
153+
148154
Ok(id)
149155
}
150156

151157
#[tauri::command]
152158
pub fn list_snapshots(room_id: String) -> Result<Vec<Snapshot>, String> {
153159
let conn = DB.lock().map_err(|e| e.to_string())?;
154-
160+
155161
let mut stmt = conn
156162
.prepare("SELECT id, room_id, name, description, thumbnail, created_by, created_at, '' as data FROM snapshots WHERE room_id = ?1 ORDER BY created_at DESC")
157163
.map_err(|e| e.to_string())?;
158-
164+
159165
let snapshots = stmt
160166
.query_map(rusqlite::params![&room_id], |row| {
161167
Ok(Snapshot {
@@ -172,14 +178,14 @@ pub fn list_snapshots(room_id: String) -> Result<Vec<Snapshot>, String> {
172178
.map_err(|e| e.to_string())?
173179
.collect::<Result<Vec<_>, _>>()
174180
.map_err(|e| e.to_string())?;
175-
181+
176182
Ok(snapshots)
177183
}
178184

179185
#[tauri::command]
180186
pub fn load_snapshot(id: String) -> Result<Snapshot, String> {
181187
let conn = DB.lock().map_err(|e| e.to_string())?;
182-
188+
183189
let snapshot = conn
184190
.query_row(
185191
"SELECT id, room_id, name, description, thumbnail, created_by, created_at, data FROM snapshots WHERE id = ?1",
@@ -198,36 +204,111 @@ pub fn load_snapshot(id: String) -> Result<Snapshot, String> {
198204
},
199205
)
200206
.map_err(|e| e.to_string())?;
201-
207+
202208
Ok(snapshot)
203209
}
204210

205211
#[tauri::command]
206212
pub fn delete_snapshot(id: String) -> Result<(), String> {
207213
let conn = DB.lock().map_err(|e| e.to_string())?;
208-
209-
conn.execute("DELETE FROM snapshots WHERE id = ?1", rusqlite::params![&id])
210-
.map_err(|e| e.to_string())?;
211-
214+
215+
conn.execute(
216+
"DELETE FROM snapshots WHERE id = ?1",
217+
rusqlite::params![&id],
218+
)
219+
.map_err(|e| e.to_string())?;
220+
212221
Ok(())
213222
}
214223

215224
#[tauri::command]
216-
pub fn update_snapshot_metadata(id: String, name: String, description: String) -> Result<(), String> {
225+
pub fn update_snapshot_metadata(
226+
id: String,
227+
name: String,
228+
description: String,
229+
) -> Result<(), String> {
217230
let conn = DB.lock().map_err(|e| e.to_string())?;
218-
231+
219232
conn.execute(
220233
"UPDATE snapshots SET name = ?1, description = ?2 WHERE id = ?3",
221234
rusqlite::params![&name, &description, &id],
222235
)
223236
.map_err(|e| e.to_string())?;
224-
237+
225238
Ok(())
226239
}
227240

241+
#[tauri::command]
242+
pub fn save_autosave_snapshot(
243+
room_id: String,
244+
name: Option<String>,
245+
description: Option<String>,
246+
thumbnail: Option<String>,
247+
data: String,
248+
) -> Result<String, String> {
249+
let conn = DB.lock().map_err(|e| e.to_string())?;
250+
251+
let timestamp = SystemTime::now()
252+
.duration_since(UNIX_EPOCH)
253+
.unwrap()
254+
.as_secs() as i64;
255+
256+
let final_name = name.unwrap_or_else(|| AUTOSAVE_DEFAULT_NAME.to_string());
257+
let final_description = description.unwrap_or_else(|| AUTOSAVE_DEFAULT_DESCRIPTION.to_string());
258+
let final_thumbnail = thumbnail.unwrap_or_default();
259+
260+
let existing_id_result: Result<String, rusqlite::Error> = conn.query_row(
261+
"SELECT id FROM snapshots WHERE room_id = ?1 AND created_by = ?2 LIMIT 1",
262+
rusqlite::params![&room_id, AUTOSAVE_CREATED_BY],
263+
|row| row.get(0),
264+
);
265+
266+
match existing_id_result {
267+
Ok(existing_id) => {
268+
conn.execute(
269+
"UPDATE snapshots SET name = ?1, description = ?2, thumbnail = ?3, data = ?4, created_at = ?5 WHERE id = ?6",
270+
rusqlite::params![
271+
&final_name,
272+
&final_description,
273+
&final_thumbnail,
274+
&data,
275+
timestamp,
276+
&existing_id,
277+
],
278+
)
279+
.map_err(|e| e.to_string())?;
280+
281+
Ok(existing_id)
282+
}
283+
Err(rusqlite::Error::QueryReturnedNoRows) => {
284+
let id = uuid::Uuid::new_v4().to_string();
285+
conn.execute(
286+
"INSERT INTO snapshots (id, room_id, name, description, thumbnail, created_by, created_at, data) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
287+
rusqlite::params![
288+
&id,
289+
&room_id,
290+
&final_name,
291+
&final_description,
292+
&final_thumbnail,
293+
AUTOSAVE_CREATED_BY,
294+
timestamp,
295+
&data,
296+
],
297+
)
298+
.map_err(|e| e.to_string())?;
299+
300+
Ok(id)
301+
}
302+
Err(err) => Err(err.to_string()),
303+
}
304+
}
305+
228306
// Room settings commands
229307

230-
fn get_room_settings_internal(conn: &rusqlite::Connection, room_id: &str) -> Result<RoomSettings, String> {
308+
fn get_room_settings_internal(
309+
conn: &rusqlite::Connection,
310+
room_id: &str,
311+
) -> Result<RoomSettings, String> {
231312
conn.query_row(
232313
"SELECT room_id, max_snapshots, auto_save_interval FROM room_settings WHERE room_id = ?1",
233314
rusqlite::params![room_id],
@@ -244,7 +325,7 @@ fn get_room_settings_internal(conn: &rusqlite::Connection, room_id: &str) -> Res
244325
Ok(RoomSettings {
245326
room_id: room_id.to_string(),
246327
max_snapshots: 10,
247-
auto_save_interval: 300,
328+
auto_save_interval: 60,
248329
})
249330
})
250331
}
@@ -256,15 +337,19 @@ pub fn get_room_settings(room_id: String) -> Result<RoomSettings, String> {
256337
}
257338

258339
#[tauri::command]
259-
pub fn update_room_settings(room_id: String, max_snapshots: i32, auto_save_interval: i32) -> Result<(), String> {
340+
pub fn update_room_settings(
341+
room_id: String,
342+
max_snapshots: i32,
343+
auto_save_interval: i32,
344+
) -> Result<(), String> {
260345
let conn = DB.lock().map_err(|e| e.to_string())?;
261-
346+
262347
conn.execute(
263348
"INSERT INTO room_settings (room_id, max_snapshots, auto_save_interval) VALUES (?1, ?2, ?3)
264349
ON CONFLICT(room_id) DO UPDATE SET max_snapshots = ?2, auto_save_interval = ?3",
265350
rusqlite::params![&room_id, max_snapshots, auto_save_interval],
266351
)
267352
.map_err(|e| e.to_string())?;
268-
353+
269354
Ok(())
270355
}

excalidraw-app/src-tauri/src/db.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
use once_cell::sync::Lazy;
12
use rusqlite::{Connection, Result};
23
use serde::{Deserialize, Serialize};
3-
use std::sync::Mutex;
4-
use once_cell::sync::Lazy;
54
use std::path::PathBuf;
5+
use std::sync::Mutex;
66

77
#[derive(Debug, Serialize, Deserialize)]
88
pub struct Drawing {
@@ -43,10 +43,10 @@ fn get_db_path() -> PathBuf {
4343
let home = std::env::var("HOME")
4444
.or_else(|_| std::env::var("USERPROFILE"))
4545
.unwrap_or_else(|_| ".".to_string());
46-
46+
4747
let app_dir = PathBuf::from(home).join(".excalidraw");
4848
std::fs::create_dir_all(&app_dir).ok();
49-
49+
5050
app_dir.join("drawings.db")
5151
}
5252

@@ -61,7 +61,7 @@ fn init_db(conn: &Connection) -> Result<()> {
6161
)",
6262
[],
6363
)?;
64-
64+
6565
conn.execute(
6666
"CREATE TABLE IF NOT EXISTS snapshots (
6767
id TEXT PRIMARY KEY,
@@ -75,15 +75,15 @@ fn init_db(conn: &Connection) -> Result<()> {
7575
)",
7676
[],
7777
)?;
78-
78+
7979
conn.execute(
8080
"CREATE TABLE IF NOT EXISTS room_settings (
8181
room_id TEXT PRIMARY KEY,
8282
max_snapshots INTEGER DEFAULT 10,
83-
auto_save_interval INTEGER DEFAULT 300
83+
auto_save_interval INTEGER DEFAULT 60
8484
)",
8585
[],
8686
)?;
87-
87+
8888
Ok(())
8989
}

0 commit comments

Comments
 (0)