1mod backend;
27mod change;
28mod content;
29mod db;
30mod directory;
31mod mutate;
32mod resolve;
33mod search;
34mod trash;
35mod workspace;
36
37use std::path::Path;
38
39use fslite_core::{
40 BatchOperation, BatchResult, Capability, Change, ChangeCursor, ContentQuery, CopyOptions,
41 CreateOptions, FileRead, FindQuery, FsError, FsResult, LinkTarget, MoveOptions,
42 MutationOptions, Node, Page, PageRequest, ReadOptions, RemoveOptions, RequestContext,
43 SearchMatch, StatOptions, TouchOptions, TrashEntry, TrashId, TreeEntry, TreeOptions,
44 VirtualPath, WorkspaceId, WorkspaceUsage, WriteOptions, WriteSource,
45};
46
47pub use db::ConnectOptions;
48pub use fslite_core::FileSystem;
49pub use workspace::{Workspace, WorkspaceOptions};
50
51pub struct SqliteFileSystem {
56 conn: tokio_rusqlite::Connection,
57}
58
59impl SqliteFileSystem {
60 pub async fn open(path: impl AsRef<Path>, options: ConnectOptions) -> FsResult<Self> {
62 let conn = db::open_file(path.as_ref(), options).await?;
63 Ok(Self { conn })
64 }
65
66 pub async fn open_in_memory(options: ConnectOptions) -> FsResult<Self> {
68 let conn = db::open_memory(options).await?;
69 Ok(Self { conn })
70 }
71
72 pub async fn create_workspace(&self, options: WorkspaceOptions) -> FsResult<Workspace> {
74 workspace::create_workspace(&self.conn, options).await
75 }
76
77 pub async fn delete_workspace(&self, workspace_id: WorkspaceId) -> FsResult<()> {
79 workspace::delete_workspace(&self.conn, workspace_id).await
80 }
81
82 pub async fn workspace_usage(&self, ctx: &RequestContext) -> FsResult<WorkspaceUsage> {
84 require_capability(ctx, Capability::Read, ctx.workspace_id)?;
85 workspace::workspace_usage(&self.conn, ctx.workspace_id).await
86 }
87
88 pub async fn stat(
90 &self,
91 ctx: &RequestContext,
92 path: &VirtualPath,
93 options: StatOptions,
94 ) -> FsResult<Node> {
95 require_capability(ctx, Capability::Read, path)?;
96 directory::stat(
97 &self.conn,
98 ctx.workspace_id,
99 path.clone(),
100 options.follow_symlinks,
101 )
102 .await
103 }
104
105 pub async fn exists(
107 &self,
108 ctx: &RequestContext,
109 path: &VirtualPath,
110 options: StatOptions,
111 ) -> FsResult<bool> {
112 require_capability(ctx, Capability::Read, path)?;
113 directory::exists(
114 &self.conn,
115 ctx.workspace_id,
116 path.clone(),
117 options.follow_symlinks,
118 )
119 .await
120 }
121
122 pub async fn read_dir(
124 &self,
125 ctx: &RequestContext,
126 path: &VirtualPath,
127 page: PageRequest,
128 ) -> FsResult<Page<Node>> {
129 require_capability(ctx, Capability::Read, path)?;
130 directory::read_dir(&self.conn, ctx.workspace_id, path.clone(), page).await
131 }
132
133 pub async fn tree(
135 &self,
136 ctx: &RequestContext,
137 path: &VirtualPath,
138 options: TreeOptions,
139 page: PageRequest,
140 ) -> FsResult<Page<TreeEntry>> {
141 require_capability(ctx, Capability::Read, path)?;
142 directory::tree(&self.conn, ctx.workspace_id, path.clone(), options, page).await
143 }
144
145 pub async fn mkdir(
147 &self,
148 ctx: &RequestContext,
149 path: &VirtualPath,
150 options: CreateOptions,
151 ) -> FsResult<Node> {
152 require_capability(ctx, Capability::Write, path)?;
153 directory::mkdir(
154 &self.conn,
155 ctx.workspace_id,
156 path.clone(),
157 options,
158 actor_json(ctx),
159 )
160 .await
161 }
162
163 pub async fn read(
165 &self,
166 ctx: &RequestContext,
167 path: &VirtualPath,
168 options: ReadOptions,
169 ) -> FsResult<FileRead> {
170 require_capability(ctx, Capability::Read, path)?;
171 content::read(&self.conn, ctx.workspace_id, path.clone(), options).await
172 }
173
174 pub async fn write(
176 &self,
177 ctx: &RequestContext,
178 path: &VirtualPath,
179 source: WriteSource,
180 options: WriteOptions,
181 ) -> FsResult<Node> {
182 require_capability(ctx, Capability::Write, path)?;
183 content::write(
184 &self.conn,
185 ctx.workspace_id,
186 path.clone(),
187 source,
188 options,
189 actor_json(ctx),
190 )
191 .await
192 }
193
194 pub async fn write_at(
196 &self,
197 ctx: &RequestContext,
198 path: &VirtualPath,
199 offset: u64,
200 source: WriteSource,
201 options: WriteOptions,
202 ) -> FsResult<Node> {
203 require_capability(ctx, Capability::Write, path)?;
204 content::write_at(
205 &self.conn,
206 ctx.workspace_id,
207 path.clone(),
208 offset,
209 source,
210 options,
211 actor_json(ctx),
212 )
213 .await
214 }
215
216 pub async fn append(
218 &self,
219 ctx: &RequestContext,
220 path: &VirtualPath,
221 source: WriteSource,
222 options: WriteOptions,
223 ) -> FsResult<Node> {
224 require_capability(ctx, Capability::Write, path)?;
225 content::append(
226 &self.conn,
227 ctx.workspace_id,
228 path.clone(),
229 source,
230 options,
231 actor_json(ctx),
232 )
233 .await
234 }
235
236 pub async fn truncate(
238 &self,
239 ctx: &RequestContext,
240 path: &VirtualPath,
241 length: u64,
242 options: MutationOptions,
243 ) -> FsResult<Node> {
244 require_capability(ctx, Capability::Write, path)?;
245 content::truncate(
246 &self.conn,
247 ctx.workspace_id,
248 path.clone(),
249 length,
250 options,
251 actor_json(ctx),
252 )
253 .await
254 }
255
256 pub async fn touch(
258 &self,
259 ctx: &RequestContext,
260 path: &VirtualPath,
261 options: TouchOptions,
262 ) -> FsResult<Node> {
263 require_capability(ctx, Capability::Write, path)?;
264 content::touch(
265 &self.conn,
266 ctx.workspace_id,
267 path.clone(),
268 options,
269 actor_json(ctx),
270 )
271 .await
272 }
273
274 pub async fn copy(
276 &self,
277 ctx: &RequestContext,
278 from: &VirtualPath,
279 to: &VirtualPath,
280 options: CopyOptions,
281 ) -> FsResult<Node> {
282 require_capability(ctx, Capability::Write, to)?;
283 mutate::copy(
284 &self.conn,
285 ctx.workspace_id,
286 from.clone(),
287 to.clone(),
288 options,
289 actor_json(ctx),
290 )
291 .await
292 }
293
294 pub async fn move_path(
296 &self,
297 ctx: &RequestContext,
298 from: &VirtualPath,
299 to: &VirtualPath,
300 options: MoveOptions,
301 ) -> FsResult<Node> {
302 require_capability(ctx, Capability::Write, to)?;
303 mutate::move_path(
304 &self.conn,
305 ctx.workspace_id,
306 from.clone(),
307 to.clone(),
308 options,
309 actor_json(ctx),
310 )
311 .await
312 }
313
314 pub async fn remove(
316 &self,
317 ctx: &RequestContext,
318 path: &VirtualPath,
319 options: RemoveOptions,
320 ) -> FsResult<()> {
321 require_capability(ctx, Capability::Delete, path)?;
322 mutate::remove(
323 &self.conn,
324 ctx.workspace_id,
325 path.clone(),
326 options,
327 actor_json(ctx),
328 )
329 .await
330 }
331
332 pub async fn symlink(
334 &self,
335 ctx: &RequestContext,
336 target: &LinkTarget,
337 link: &VirtualPath,
338 options: CreateOptions,
339 ) -> FsResult<Node> {
340 require_capability(ctx, Capability::Write, link)?;
341 mutate::symlink(
342 &self.conn,
343 ctx.workspace_id,
344 target.clone(),
345 link.clone(),
346 options,
347 actor_json(ctx),
348 )
349 .await
350 }
351
352 pub async fn read_link(
354 &self,
355 ctx: &RequestContext,
356 path: &VirtualPath,
357 ) -> FsResult<LinkTarget> {
358 require_capability(ctx, Capability::Read, path)?;
359 mutate::read_link(&self.conn, ctx.workspace_id, path.clone()).await
360 }
361
362 pub async fn trash(
364 &self,
365 ctx: &RequestContext,
366 path: &VirtualPath,
367 options: MutationOptions,
368 ) -> FsResult<TrashEntry> {
369 require_capability(ctx, Capability::TrashRestore, path)?;
370 trash::trash(
371 &self.conn,
372 ctx.workspace_id,
373 path.clone(),
374 options,
375 actor_json(ctx),
376 )
377 .await
378 }
379
380 pub async fn list_trash(
382 &self,
383 ctx: &RequestContext,
384 page: PageRequest,
385 ) -> FsResult<Page<TrashEntry>> {
386 require_capability(ctx, Capability::Read, ctx.workspace_id)?;
387 trash::list_trash(&self.conn, ctx.workspace_id, page).await
388 }
389
390 pub async fn restore(
392 &self,
393 ctx: &RequestContext,
394 trash_id: TrashId,
395 destination: Option<&VirtualPath>,
396 options: MutationOptions,
397 ) -> FsResult<Node> {
398 require_capability(ctx, Capability::TrashRestore, ctx.workspace_id)?;
399 trash::restore(
400 &self.conn,
401 ctx.workspace_id,
402 trash_id,
403 destination.cloned(),
404 options,
405 actor_json(ctx),
406 )
407 .await
408 }
409
410 pub async fn purge(&self, ctx: &RequestContext, trash_id: TrashId) -> FsResult<()> {
412 require_capability(ctx, Capability::Delete, ctx.workspace_id)?;
413 trash::purge(&self.conn, ctx.workspace_id, trash_id, actor_json(ctx)).await
414 }
415
416 pub async fn set_attribute(
418 &self,
419 ctx: &RequestContext,
420 path: &VirtualPath,
421 key: &str,
422 value: &[u8],
423 options: MutationOptions,
424 ) -> FsResult<Node> {
425 require_capability(ctx, Capability::Write, path)?;
426 mutate::set_attribute(
427 &self.conn,
428 ctx.workspace_id,
429 path.clone(),
430 key.to_string(),
431 value.to_vec(),
432 options,
433 actor_json(ctx),
434 )
435 .await
436 }
437
438 pub async fn remove_attribute(
440 &self,
441 ctx: &RequestContext,
442 path: &VirtualPath,
443 key: &str,
444 options: MutationOptions,
445 ) -> FsResult<Node> {
446 require_capability(ctx, Capability::Write, path)?;
447 mutate::remove_attribute(
448 &self.conn,
449 ctx.workspace_id,
450 path.clone(),
451 key.to_string(),
452 options,
453 actor_json(ctx),
454 )
455 .await
456 }
457
458 pub async fn batch(
460 &self,
461 ctx: &RequestContext,
462 operations: Vec<BatchOperation>,
463 ) -> FsResult<Vec<BatchResult>> {
464 mutate::batch(&self.conn, ctx, operations, actor_json(ctx)).await
465 }
466
467 pub async fn glob(
469 &self,
470 ctx: &RequestContext,
471 pattern: &str,
472 page: PageRequest,
473 ) -> FsResult<Page<Node>> {
474 require_capability(ctx, Capability::Read, ctx.workspace_id)?;
475 search::glob(&self.conn, ctx.workspace_id, pattern.to_string(), page).await
476 }
477
478 pub async fn find(
480 &self,
481 ctx: &RequestContext,
482 query: FindQuery,
483 page: PageRequest,
484 ) -> FsResult<Page<Node>> {
485 require_capability(ctx, Capability::Read, ctx.workspace_id)?;
486 search::find(&self.conn, ctx.workspace_id, query, page).await
487 }
488
489 pub async fn search_content(
491 &self,
492 ctx: &RequestContext,
493 query: ContentQuery,
494 page: PageRequest,
495 ) -> FsResult<Page<SearchMatch>> {
496 require_capability(ctx, Capability::Read, ctx.workspace_id)?;
497 search::search_content(&self.conn, ctx.workspace_id, query, page).await
498 }
499
500 pub async fn changes(
502 &self,
503 ctx: &RequestContext,
504 after: Option<ChangeCursor>,
505 page: PageRequest,
506 ) -> FsResult<Page<Change>> {
507 require_capability(ctx, Capability::Read, ctx.workspace_id)?;
508 change::changes(&self.conn, ctx.workspace_id, after, page).await
509 }
510}
511
512fn actor_json(ctx: &RequestContext) -> String {
513 serde_json::to_string(&ctx.actor_metadata).unwrap_or_else(|_| "{}".to_string())
514}
515
516fn require_capability(
517 ctx: &RequestContext,
518 capability: Capability,
519 subject: impl std::fmt::Display,
520) -> FsResult<()> {
521 if ctx.has_capability(capability) {
522 Ok(())
523 } else {
524 Err(FsError::permission_denied(subject))
525 }
526}