// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! The user's files, asynchronously and alike on every platform: natively through `std::fs` on a worker thread of ltk's, in a browser through forge/web's file requests (forge/web/DESIGN.md, section 11). Every operation hands its result to `done`, whose message reaches the app through its [`ChannelSender`], so nothing blocks the app's thread. Results come in no set order, so an app that may have moved on checks that a result is still the one it waits for. //! //! Contents travel in chunks: a large file is read in ranges with [`read_range`] and written through a [`Writer`], one chunk after the other is answered, [`CHUNK`] bytes at a time; [`read`] and [`write()`] are for small files. A copy runs where the files are, its contents never passing through the app. //! //! Errors are `io::Error`s of the same kinds on every platform: `NotFound`, `AlreadyExists`, `NotADirectory`, `IsADirectory`, `DirectoryNotEmpty`, `PermissionDenied`, `Unsupported`, and `Interrupted` for a cancelled copy. //! //! An app's own resources are read through [`crate::vfs`], not here. pub mod callback; #[ cfg( not( target_arch = "wasm32" ) ) ] mod copy; mod mime; #[ cfg( not( target_arch = "wasm32" ) ) ] mod native; #[ cfg( all( target_arch = "wasm32", not( feature = "web" ) ) ) ] mod unsupported; #[ cfg( not( target_arch = "wasm32" ) ) ] use native as backend; #[ cfg( all( target_arch = "wasm32", feature = "web" ) ) ] use crate::platform::web::files as backend; #[ cfg( all( target_arch = "wasm32", not( feature = "web" ) ) ) ] use unsupported as backend; use std::ffi::OsString; use std::io; use std::path::PathBuf; use std::time::SystemTime; use crate::ChannelSender; pub use mime::mime_type; /// The size of the chunks a large file is best read and written in. pub const CHUNK: usize = 1 << 20; #[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] pub enum Kind { File, Directory, } /// One entry of a directory. #[ derive( Debug, Clone ) ] pub struct Entry { pub name: OsString, pub kind: Kind, /// A file's size in bytes; `0` for a directory. pub size: u64, pub modified: Option, /// `None` in a browser, which does not keep it. pub accessed: Option, } #[ derive( Debug, Clone ) ] pub struct Metadata { pub kind: Kind, pub size: u64, pub modified: Option, pub accessed: Option, } /// A folder of the user's computer, mounted in a browser. #[ derive( Debug, Clone, PartialEq, Eq ) ] pub struct Mount { pub path: PathBuf, pub name: String, } pub( crate ) type Reply = Box ) + Send>; pub( crate ) type Progress = Box; fn reply( sender: &ChannelSender, done: impl FnOnce( io::Result ) -> M + Send + 'static ) -> Reply { let sender = sender.clone(); Box::new( move |result| { let _ = sender.send( done( result ) ); } ) } /// The entries of the directory `path`, in no order. pub fn list( path: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result> ) -> M + Send + 'static ) { backend::list( path.into(), reply( sender, done ) ); } pub fn stat( path: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result ) -> M + Send + 'static ) { backend::stat( path.into(), reply( sender, done ) ); } /// The whole of a small file. pub fn read( path: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result> ) -> M + Send + 'static ) { backend::read( path.into(), None, reply( sender, done ) ); } /// Up to `length` bytes of the file from `offset`; fewer at its end, none past it. pub fn read_range( path: impl Into, offset: u64, length: u64, sender: &ChannelSender, done: impl FnOnce( io::Result> ) -> M + Send + 'static ) { backend::read( path.into(), Some( ( offset, length ) ), reply( sender, done ) ); } /// Create or replace a small file with `data`, whole or not at all. pub fn write( path: impl Into, data: Vec, sender: &ChannelSender, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static ) { backend::write( path.into(), data, reply( sender, done ) ); } /// A [`Writer`] for the file at `path`. pub fn open_write( path: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result ) -> M + Send + 'static ) { let done = reply( sender, done ); backend::open_write( path.into(), Box::new( move |result| done( result.map( |handle| Writer { handle: Some( handle ) } ) ) ) ); } pub fn create_dir( path: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static ) { backend::create_dir( path.into(), reply( sender, done ) ); } /// Move `from` to `to`, which must not exist; in a browser, within one mount. pub fn rename( from: impl Into, to: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static ) { backend::rename( from.into(), to.into(), reply( sender, done ) ); } /// Remove a file or an empty directory. pub fn remove( path: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static ) { backend::remove( path.into(), false, reply( sender, done ) ); } /// Remove a file, or a directory and everything in it. pub fn remove_all( path: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static ) { backend::remove( path.into(), true, reply( sender, done ) ); } /// Copy the file `from` to `to`, which must not exist and appears only once whole. `progress` hears of the bytes copied out of the total every so often. pub fn copy( from: impl Into, to: impl Into, sender: &ChannelSender, progress: impl Fn( u64, u64 ) -> M + Send + 'static, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static, ) -> Operation { start_copy( from.into(), to.into(), false, sender, progress, done ) } /// Copy the directory `from` and everything in it to `to`, which must not exist; a copy that fails or is cancelled leaves no `to` behind. pub fn copy_all( from: impl Into, to: impl Into, sender: &ChannelSender, progress: impl Fn( u64, u64 ) -> M + Send + 'static, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static, ) -> Operation { start_copy( from.into(), to.into(), true, sender, progress, done ) } fn start_copy( from: PathBuf, to: PathBuf, all: bool, sender: &ChannelSender, progress: impl Fn( u64, u64 ) -> M + Send + 'static, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static, ) -> Operation { let progress_sender = sender.clone(); let progress: Progress = Box::new( move |copied, total| { let _ = progress_sender.send( progress( copied, total ) ); } ); Operation { handle: backend::copy( from, to, all, progress, reply( sender, done ) ) } } /// Have the browser ask the user for a folder of their computer and mount it. Call it from the user's press, since the browser opens its picker only then; natively it fails with `Unsupported`. pub fn mount( sender: &ChannelSender, done: impl FnOnce( io::Result ) -> M + Send + 'static ) { backend::mount( reply( sender, done ) ); } pub fn unmount( path: impl Into, sender: &ChannelSender, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static ) { backend::unmount( path.into(), reply( sender, done ) ); } static MOUNTS: std::sync::Mutex>> = std::sync::Mutex::new( None ); /// The folders mounted in a browser, as the desktop last said; `None` natively, where nothing is mounted, and before the desktop has said. pub fn mounts() -> Option> { MOUNTS.lock().ok()?.clone() } #[ cfg_attr( not( feature = "web" ), allow( dead_code ) ) ] pub( crate ) fn set_mounts( mounts: &[ Mount ] ) { if let Ok( mut held ) = MOUNTS.lock() { *held = Some( mounts.to_vec() ); } } /// A file being written in chunks, whose contents appear only once closed; in a browser a new file shows empty until then. Dropped without [`Writer::close`], it is aborted and the file left as it was. pub struct Writer { handle: Option, } impl Writer { /// Append `chunk`; send the next once `done` has come. pub fn write( &self, chunk: Vec, sender: &ChannelSender, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static ) { let done = reply( sender, done ); match &self.handle { Some( handle ) => backend::write_chunk( handle, chunk, done ), None => done( Err( io::ErrorKind::BrokenPipe.into() ) ), } } pub fn close( mut self, sender: &ChannelSender, done: impl FnOnce( io::Result<()> ) -> M + Send + 'static ) { let done = reply( sender, done ); match self.handle.take() { Some( handle ) => backend::close( handle, done ), None => done( Err( io::ErrorKind::BrokenPipe.into() ) ), } } pub fn abort( mut self ) { if let Some( handle ) = self.handle.take() { backend::abort( handle ); } } } impl Drop for Writer { fn drop( &mut self ) { if let Some( handle ) = self.handle.take() { backend::abort( handle ); } } } /// A copy in progress. pub struct Operation { handle: backend::OperationHandle, } impl Operation { /// Stop the copy, whose `done` then comes with `ErrorKind::Interrupted`. pub fn cancel( &self ) { backend::cancel( &self.handle ); } }