mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
implemented write
This commit is contained in:
1 parent
e9c8ddd18b
commit
5ac94848f3
1 file changed
+559
-11
+559
-11
@@ -9,6 +9,14 @@
|
|||||||
//! cursor ([`ProtonDriveClient::enumerate_events`]) and pushes invalidations
|
//! cursor ([`ProtonDriveClient::enumerate_events`]) and pushes invalidations
|
||||||
//! into the kernel via a fuser [`Notifier`], so the cache TTL can be long while
|
//! into the kernel via a fuser [`Notifier`], so the cache TTL can be long while
|
||||||
//! remote changes still show up promptly.
|
//! remote changes still show up promptly.
|
||||||
|
//!
|
||||||
|
//! Phase 3 makes the mount writable. Each file opened for writing gets a
|
||||||
|
//! [`WriteHandle`] whose buffer accumulates the full plaintext; on flush/release
|
||||||
|
//! the buffer is sealed as a new revision via
|
||||||
|
//! [`ProtonDriveClient::upload_new_revision`] (the SDK uploads whole revisions,
|
||||||
|
//! not byte ranges). New files are created empty up front so they get a real
|
||||||
|
//! uid; namespace ops map to `create_folder`, `trash_nodes`, `rename_node` and
|
||||||
|
//! `move_node`.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
@@ -17,9 +25,10 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use fuser::{
|
use fuser::{
|
||||||
Config, Errno, FileAttr, FileHandle, FileType, Filesystem, FopenFlags, Generation, INodeNo,
|
BsdFileFlags, Config, Errno, FileAttr, FileHandle, FileType, Filesystem, FopenFlags, Generation,
|
||||||
LockOwner, MountOption, Notifier, OpenFlags, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry,
|
INodeNo, LockOwner, MountOption, Notifier, OpenAccMode, OpenFlags, RenameFlags, ReplyAttr,
|
||||||
ReplyOpen, Request, Session,
|
ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyWrite, Request,
|
||||||
|
Session, TimeOrNow, WriteFlags,
|
||||||
};
|
};
|
||||||
use proton_drive_rs::proton_sdk::ids::{DriveEventId, NodeUid};
|
use proton_drive_rs::proton_sdk::ids::{DriveEventId, NodeUid};
|
||||||
use proton_drive_rs::{DriveEvent, DriveEventScopeId, Node, NodeKind, ProtonDriveClient};
|
use proton_drive_rs::{DriveEvent, DriveEventScopeId, Node, NodeKind, ProtonDriveClient};
|
||||||
@@ -43,6 +52,21 @@ struct Entry {
|
|||||||
node: Node,
|
node: Node,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Buffered state for a file opened for writing. The full plaintext is
|
||||||
|
/// assembled here and uploaded as one new revision on flush/release, because
|
||||||
|
/// the SDK seals whole revisions rather than byte ranges.
|
||||||
|
struct WriteHandle {
|
||||||
|
ino: u64,
|
||||||
|
uid: NodeUid,
|
||||||
|
/// The plaintext being assembled; valid only once `seeded`.
|
||||||
|
buf: Vec<u8>,
|
||||||
|
/// Whether `buf` holds the file's current content (or was emptied by a
|
||||||
|
/// truncate). Until then a partial write must hydrate the base first.
|
||||||
|
seeded: bool,
|
||||||
|
/// Whether `buf` diverged from the remote and needs an upload.
|
||||||
|
dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Mutable inode bookkeeping, guarded by a mutex because fuser drives the
|
/// Mutable inode bookkeeping, guarded by a mutex because fuser drives the
|
||||||
/// `Filesystem` trait through `&self`.
|
/// `Filesystem` trait through `&self`.
|
||||||
struct State {
|
struct State {
|
||||||
@@ -54,6 +78,10 @@ struct State {
|
|||||||
/// key means the directory has been enumerated.
|
/// key means the directory has been enumerated.
|
||||||
children: HashMap<u64, Vec<u64>>,
|
children: HashMap<u64, Vec<u64>>,
|
||||||
next_ino: u64,
|
next_ino: u64,
|
||||||
|
/// Open write handles keyed by file handle id. Read-only opens use fh 0 and
|
||||||
|
/// have no entry here.
|
||||||
|
handles: HashMap<u64, WriteHandle>,
|
||||||
|
next_fh: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
@@ -86,6 +114,23 @@ impl State {
|
|||||||
}
|
}
|
||||||
Some((entry.parent, entry.node.name))
|
Some((entry.parent, entry.node.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a file entry's recorded plaintext size so `getattr` reflects an
|
||||||
|
/// in-progress write before the new revision is sealed.
|
||||||
|
fn set_size(&mut self, ino: u64, size: u64) {
|
||||||
|
if let Some(e) = self.entries.get_mut(&ino)
|
||||||
|
&& let NodeKind::File { claimed_size, .. } = &mut e.node.kind
|
||||||
|
{
|
||||||
|
*claimed_size = Some(size as i64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a file entry's modification time (epoch seconds).
|
||||||
|
fn touch_mtime(&mut self, ino: u64, secs: i64) {
|
||||||
|
if let Some(e) = self.entries.get_mut(&ino) {
|
||||||
|
e.node.modification_time = secs;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Proton Drive VFS. FUSE callbacks are synchronous, so the Tokio handle
|
/// The Proton Drive VFS. FUSE callbacks are synchronous, so the Tokio handle
|
||||||
@@ -119,6 +164,8 @@ impl ProtonFs {
|
|||||||
by_uid,
|
by_uid,
|
||||||
children: HashMap::new(),
|
children: HashMap::new(),
|
||||||
next_ino: 2,
|
next_ino: 2,
|
||||||
|
handles: HashMap::new(),
|
||||||
|
next_fh: 1,
|
||||||
})),
|
})),
|
||||||
uid,
|
uid,
|
||||||
gid,
|
gid,
|
||||||
@@ -178,8 +225,8 @@ impl ProtonFs {
|
|||||||
|
|
||||||
fn attr(&self, ino: u64, node: &Node) -> FileAttr {
|
fn attr(&self, ino: u64, node: &Node) -> FileAttr {
|
||||||
let (kind, perm) = match node.kind {
|
let (kind, perm) = match node.kind {
|
||||||
NodeKind::Folder => (FileType::Directory, 0o555),
|
NodeKind::Folder => (FileType::Directory, 0o755),
|
||||||
NodeKind::File { .. } => (FileType::RegularFile, 0o444),
|
NodeKind::File { .. } => (FileType::RegularFile, 0o644),
|
||||||
};
|
};
|
||||||
let size = node_size(node);
|
let size = node_size(node);
|
||||||
let mtime = unix_secs(node.modification_time);
|
let mtime = unix_secs(node.modification_time);
|
||||||
@@ -202,6 +249,102 @@ impl ProtonFs {
|
|||||||
flags: 0,
|
flags: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a child `name` within `parent` to its `(inode, uid)`, ensuring
|
||||||
|
/// the parent's listing is cached first.
|
||||||
|
fn lookup_child(&self, parent: u64, name: &str) -> Result<(u64, NodeUid), Errno> {
|
||||||
|
self.ensure_children(parent)?;
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
st.children
|
||||||
|
.get(&parent)
|
||||||
|
.and_then(|kids| {
|
||||||
|
kids.iter().copied().find_map(|ino| {
|
||||||
|
st.entries
|
||||||
|
.get(&ino)
|
||||||
|
.filter(|e| e.node.name == name)
|
||||||
|
.map(|e| (ino, e.uid.clone()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok_or(Errno::ENOENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch a single node's current metadata from the remote.
|
||||||
|
fn fetch_node(&self, uid: &NodeUid) -> Result<Node, Errno> {
|
||||||
|
let nodes = self
|
||||||
|
.rt
|
||||||
|
.block_on(self.client.enumerate_nodes(std::slice::from_ref(uid)))
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(%uid, error = %e, "enumerate node failed");
|
||||||
|
Errno::EIO
|
||||||
|
})?;
|
||||||
|
nodes.into_iter().next().ok_or(Errno::ENOENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a write handle's buffer as a new revision if it is dirty, clearing
|
||||||
|
/// the dirty flag and refreshing the entry's size/mtime on success. No-op for
|
||||||
|
/// a clean (or unknown) handle. Network I/O happens without the lock held.
|
||||||
|
fn commit(&self, fh: u64) -> Result<(), Errno> {
|
||||||
|
let (uid, buf, ino) = {
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
match st.handles.get(&fh) {
|
||||||
|
Some(h) if h.dirty => (h.uid.clone(), h.buf.clone(), h.ino),
|
||||||
|
_ => return Ok(()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.rt
|
||||||
|
.block_on(self.client.upload_new_revision(&uid, &buf))
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(%uid, error = %e, "upload new revision failed");
|
||||||
|
Errno::EIO
|
||||||
|
})?;
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs() as i64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
if let Some(h) = st.handles.get_mut(&fh) {
|
||||||
|
h.dirty = false;
|
||||||
|
}
|
||||||
|
st.set_size(ino, buf.len() as u64);
|
||||||
|
st.touch_mtime(ino, now);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trash the child `name` under `parent` on the remote, then drop it from the
|
||||||
|
/// local cache. Backs both `unlink` and `rmdir` (Proton trashes whole
|
||||||
|
/// subtrees, so an `rmdir` of a non-empty dir behaves the same).
|
||||||
|
fn trash_child(&self, parent: u64, name: &str, reply: ReplyEmpty) {
|
||||||
|
let (_ino, uid) = match self.lookup_child(parent, name) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => {
|
||||||
|
reply.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = self.rt.block_on(self.client.trash_nodes(std::slice::from_ref(&uid))) {
|
||||||
|
error!(%uid, error = %e, "trash failed");
|
||||||
|
reply.error(Errno::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.state.lock().unwrap().forget(&uid);
|
||||||
|
reply.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A coarse MIME type guessed from a file name's extension; Proton stores this
|
||||||
|
/// on the node but an exact value is not required for correctness.
|
||||||
|
fn media_type_for(name: &str) -> &'static str {
|
||||||
|
let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase());
|
||||||
|
match ext.as_deref() {
|
||||||
|
Some("txt" | "md" | "log") => "text/plain",
|
||||||
|
Some("png") => "image/png",
|
||||||
|
Some("jpg" | "jpeg") => "image/jpeg",
|
||||||
|
Some("gif") => "image/gif",
|
||||||
|
Some("pdf") => "application/pdf",
|
||||||
|
Some("json") => "application/json",
|
||||||
|
Some("html" | "htm") => "text/html",
|
||||||
|
_ => "application/octet-stream",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The plaintext size, in bytes, that a node reports.
|
/// The plaintext size, in bytes, that a node reports.
|
||||||
@@ -304,13 +447,35 @@ impl Filesystem for ProtonFs {
|
|||||||
reply.ok();
|
reply.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open(&self, _req: &Request, ino: INodeNo, _flags: OpenFlags, reply: ReplyOpen) {
|
fn open(&self, _req: &Request, ino: INodeNo, flags: OpenFlags, reply: ReplyOpen) {
|
||||||
|
let uid = {
|
||||||
let st = self.state.lock().unwrap();
|
let st = self.state.lock().unwrap();
|
||||||
match st.entries.get(&ino.0) {
|
match st.entries.get(&ino.0) {
|
||||||
Some(e) if e.node.is_file() => reply.opened(FileHandle(0), FopenFlags::empty()),
|
Some(e) if e.node.is_file() => e.uid.clone(),
|
||||||
Some(_) => reply.error(Errno::EISDIR),
|
Some(_) => {
|
||||||
None => reply.error(Errno::ENOENT),
|
reply.error(Errno::EISDIR);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
reply.error(Errno::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Read-only opens stay stateless (fh 0). A write open allocates a buffer
|
||||||
|
// handle, hydrated lazily on the first write that needs the base content.
|
||||||
|
if flags.acc_mode() == OpenAccMode::O_RDONLY {
|
||||||
|
reply.opened(FileHandle(0), FopenFlags::empty());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
let fh = st.next_fh;
|
||||||
|
st.next_fh += 1;
|
||||||
|
st.handles.insert(
|
||||||
|
fh,
|
||||||
|
WriteHandle { ino: ino.0, uid, buf: Vec::new(), seeded: false, dirty: false },
|
||||||
|
);
|
||||||
|
reply.opened(FileHandle(fh), FopenFlags::empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read(
|
fn read(
|
||||||
@@ -326,6 +491,14 @@ impl Filesystem for ProtonFs {
|
|||||||
) {
|
) {
|
||||||
let uid = {
|
let uid = {
|
||||||
let st = self.state.lock().unwrap();
|
let st = self.state.lock().unwrap();
|
||||||
|
// If the file is open for writing and its buffer is populated, serve
|
||||||
|
// from there so reads see the in-flight (possibly unsaved) content.
|
||||||
|
if let Some(h) = st.handles.values().find(|h| h.ino == ino.0 && h.seeded) {
|
||||||
|
let off = (offset as usize).min(h.buf.len());
|
||||||
|
let end = (off + size as usize).min(h.buf.len());
|
||||||
|
reply.data(&h.buf[off..end]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
match st.entries.get(&ino.0) {
|
match st.entries.get(&ino.0) {
|
||||||
Some(e) if e.node.is_file() => e.uid.clone(),
|
Some(e) if e.node.is_file() => e.uid.clone(),
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
@@ -349,6 +522,383 @@ impl Filesystem for ProtonFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
&self,
|
||||||
|
_req: &Request,
|
||||||
|
parent: INodeNo,
|
||||||
|
name: &OsStr,
|
||||||
|
_mode: u32,
|
||||||
|
_umask: u32,
|
||||||
|
_flags: i32,
|
||||||
|
reply: ReplyCreate,
|
||||||
|
) {
|
||||||
|
let parent = parent.0;
|
||||||
|
if let Err(e) = self.ensure_children(parent) {
|
||||||
|
reply.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let name = name.to_string_lossy().into_owned();
|
||||||
|
let parent_uid = {
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
match st.entries.get(&parent) {
|
||||||
|
Some(e) => e.uid.clone(),
|
||||||
|
None => {
|
||||||
|
reply.error(Errno::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Create an empty file on the remote so it has a real uid immediately;
|
||||||
|
// written bytes are buffered and sealed as a new revision on close.
|
||||||
|
let new_uid = match self.rt.block_on(self.client.upload_file(
|
||||||
|
&parent_uid,
|
||||||
|
&name,
|
||||||
|
media_type_for(&name),
|
||||||
|
b"",
|
||||||
|
)) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
error!(%parent_uid, name, error = %e, "create file failed");
|
||||||
|
reply.error(Errno::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let node = match self.fetch_node(&new_uid) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
reply.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
let ino = st.intern(parent, node);
|
||||||
|
if let Some(kids) = st.children.get_mut(&parent)
|
||||||
|
&& !kids.contains(&ino)
|
||||||
|
{
|
||||||
|
kids.push(ino);
|
||||||
|
}
|
||||||
|
let fh = st.next_fh;
|
||||||
|
st.next_fh += 1;
|
||||||
|
st.handles.insert(
|
||||||
|
fh,
|
||||||
|
WriteHandle { ino, uid: new_uid, buf: Vec::new(), seeded: true, dirty: false },
|
||||||
|
);
|
||||||
|
let attr = self.attr(ino, &st.entries.get(&ino).unwrap().node);
|
||||||
|
reply.created(&TTL, &attr, Generation(0), FileHandle(fh), FopenFlags::empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(
|
||||||
|
&self,
|
||||||
|
_req: &Request,
|
||||||
|
ino: INodeNo,
|
||||||
|
fh: FileHandle,
|
||||||
|
offset: u64,
|
||||||
|
data: &[u8],
|
||||||
|
_write_flags: WriteFlags,
|
||||||
|
_flags: OpenFlags,
|
||||||
|
_lock_owner: Option<LockOwner>,
|
||||||
|
reply: ReplyWrite,
|
||||||
|
) {
|
||||||
|
let fh = fh.0;
|
||||||
|
// Partial writes need the file's current content; hydrate it once.
|
||||||
|
let need_seed = {
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
match st.handles.get(&fh) {
|
||||||
|
Some(h) => !h.seeded,
|
||||||
|
None => {
|
||||||
|
reply.error(Errno::EBADF);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if need_seed {
|
||||||
|
let uid = self
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.handles
|
||||||
|
.get(&fh)
|
||||||
|
.map(|h| h.uid.clone());
|
||||||
|
let Some(uid) = uid else {
|
||||||
|
reply.error(Errno::EBADF);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let base = match self.rt.block_on(self.client.download_file(&uid)) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(%uid, error = %e, "seed write buffer failed");
|
||||||
|
reply.error(Errno::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
if let Some(h) = st.handles.get_mut(&fh)
|
||||||
|
&& !h.seeded
|
||||||
|
{
|
||||||
|
h.buf = base;
|
||||||
|
h.seeded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let off = offset as usize;
|
||||||
|
let new_len = {
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
let Some(h) = st.handles.get_mut(&fh) else {
|
||||||
|
reply.error(Errno::EBADF);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let end = off + data.len();
|
||||||
|
if h.buf.len() < end {
|
||||||
|
h.buf.resize(end, 0);
|
||||||
|
}
|
||||||
|
h.buf[off..end].copy_from_slice(data);
|
||||||
|
h.dirty = true;
|
||||||
|
let len = h.buf.len() as u64;
|
||||||
|
st.set_size(ino.0, len);
|
||||||
|
len
|
||||||
|
};
|
||||||
|
debug!(ino = ino.0, fh, offset, len = data.len(), new_len, "buffered write");
|
||||||
|
reply.written(data.len() as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn setattr(
|
||||||
|
&self,
|
||||||
|
_req: &Request,
|
||||||
|
ino: INodeNo,
|
||||||
|
_mode: Option<u32>,
|
||||||
|
_uid: Option<u32>,
|
||||||
|
_gid: Option<u32>,
|
||||||
|
size: Option<u64>,
|
||||||
|
_atime: Option<TimeOrNow>,
|
||||||
|
_mtime: Option<TimeOrNow>,
|
||||||
|
_ctime: Option<SystemTime>,
|
||||||
|
fh: Option<FileHandle>,
|
||||||
|
_crtime: Option<SystemTime>,
|
||||||
|
_chgtime: Option<SystemTime>,
|
||||||
|
_bkuptime: Option<SystemTime>,
|
||||||
|
_flags: Option<BsdFileFlags>,
|
||||||
|
reply: ReplyAttr,
|
||||||
|
) {
|
||||||
|
// Only resizes change remote state; everything else (mode/owner/times) is
|
||||||
|
// accepted and ignored so tools that chmod/utimes after writing succeed.
|
||||||
|
if let Some(size) = size {
|
||||||
|
let fh = fh.map(|f| f.0).filter(|&f| f != 0);
|
||||||
|
let handled = match fh {
|
||||||
|
Some(fh) => {
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
match st.handles.get_mut(&fh) {
|
||||||
|
Some(h) => {
|
||||||
|
h.buf.resize(size as usize, 0);
|
||||||
|
h.seeded = true;
|
||||||
|
h.dirty = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
if !handled {
|
||||||
|
// Path-based truncate with no open write handle: resize the
|
||||||
|
// remote content and seal it now.
|
||||||
|
let uid = {
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
match st.entries.get(&ino.0) {
|
||||||
|
Some(e) if e.node.is_file() => e.uid.clone(),
|
||||||
|
Some(_) => {
|
||||||
|
reply.error(Errno::EISDIR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
reply.error(Errno::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut content = if size == 0 {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
match self.rt.block_on(self.client.download_file(&uid)) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(%uid, error = %e, "truncate base download failed");
|
||||||
|
reply.error(Errno::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
content.resize(size as usize, 0);
|
||||||
|
if let Err(e) = self.rt.block_on(self.client.upload_new_revision(&uid, &content)) {
|
||||||
|
error!(%uid, error = %e, "truncate upload failed");
|
||||||
|
reply.error(Errno::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.state.lock().unwrap().set_size(ino.0, size);
|
||||||
|
}
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
match st.entries.get(&ino.0) {
|
||||||
|
Some(e) => {
|
||||||
|
let attr = self.attr(ino.0, &e.node);
|
||||||
|
reply.attr(&TTL, &attr);
|
||||||
|
}
|
||||||
|
None => reply.error(Errno::ENOENT),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _lock_owner: LockOwner, reply: ReplyEmpty) {
|
||||||
|
match self.commit(fh.0) {
|
||||||
|
Ok(()) => reply.ok(),
|
||||||
|
Err(e) => reply.error(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fsync(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _datasync: bool, reply: ReplyEmpty) {
|
||||||
|
match self.commit(fh.0) {
|
||||||
|
Ok(()) => reply.ok(),
|
||||||
|
Err(e) => reply.error(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release(
|
||||||
|
&self,
|
||||||
|
_req: &Request,
|
||||||
|
_ino: INodeNo,
|
||||||
|
fh: FileHandle,
|
||||||
|
_flags: OpenFlags,
|
||||||
|
_lock_owner: Option<LockOwner>,
|
||||||
|
_flush: bool,
|
||||||
|
reply: ReplyEmpty,
|
||||||
|
) {
|
||||||
|
let res = self.commit(fh.0);
|
||||||
|
self.state.lock().unwrap().handles.remove(&fh.0);
|
||||||
|
match res {
|
||||||
|
Ok(()) => reply.ok(),
|
||||||
|
Err(e) => reply.error(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlink(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
|
||||||
|
self.trash_child(parent.0, &name.to_string_lossy(), reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rmdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
|
||||||
|
self.trash_child(parent.0, &name.to_string_lossy(), reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mkdir(
|
||||||
|
&self,
|
||||||
|
_req: &Request,
|
||||||
|
parent: INodeNo,
|
||||||
|
name: &OsStr,
|
||||||
|
_mode: u32,
|
||||||
|
_umask: u32,
|
||||||
|
reply: ReplyEntry,
|
||||||
|
) {
|
||||||
|
let parent = parent.0;
|
||||||
|
if let Err(e) = self.ensure_children(parent) {
|
||||||
|
reply.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let name = name.to_string_lossy().into_owned();
|
||||||
|
let parent_uid = {
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
match st.entries.get(&parent) {
|
||||||
|
Some(e) => e.uid.clone(),
|
||||||
|
None => {
|
||||||
|
reply.error(Errno::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs() as i64)
|
||||||
|
.ok();
|
||||||
|
let new_uid = match self.rt.block_on(self.client.create_folder(&parent_uid, &name, now)) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
error!(%parent_uid, name, error = %e, "create folder failed");
|
||||||
|
reply.error(Errno::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let node = match self.fetch_node(&new_uid) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
reply.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
let ino = st.intern(parent, node);
|
||||||
|
if let Some(kids) = st.children.get_mut(&parent)
|
||||||
|
&& !kids.contains(&ino)
|
||||||
|
{
|
||||||
|
kids.push(ino);
|
||||||
|
}
|
||||||
|
let attr = self.attr(ino, &st.entries.get(&ino).unwrap().node);
|
||||||
|
reply.entry(&TTL, &attr, Generation(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename(
|
||||||
|
&self,
|
||||||
|
_req: &Request,
|
||||||
|
parent: INodeNo,
|
||||||
|
name: &OsStr,
|
||||||
|
newparent: INodeNo,
|
||||||
|
newname: &OsStr,
|
||||||
|
_flags: RenameFlags,
|
||||||
|
reply: ReplyEmpty,
|
||||||
|
) {
|
||||||
|
let parent = parent.0;
|
||||||
|
let newparent = newparent.0;
|
||||||
|
let name = name.to_string_lossy().into_owned();
|
||||||
|
let newname = newname.to_string_lossy().into_owned();
|
||||||
|
let (_ino, uid) = match self.lookup_child(parent, &name) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => {
|
||||||
|
reply.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Move first if the parent changed, then rename if the name changed.
|
||||||
|
if newparent != parent {
|
||||||
|
if let Err(e) = self.ensure_children(newparent) {
|
||||||
|
reply.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let new_parent_uid = {
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
match st.entries.get(&newparent) {
|
||||||
|
Some(e) => e.uid.clone(),
|
||||||
|
None => {
|
||||||
|
reply.error(Errno::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = self.rt.block_on(self.client.move_node(&uid, &new_parent_uid)) {
|
||||||
|
error!(%uid, error = %e, "move failed");
|
||||||
|
reply.error(Errno::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if newname != name
|
||||||
|
&& let Err(e) = self.rt.block_on(self.client.rename_node(&uid, &newname, None))
|
||||||
|
{
|
||||||
|
error!(%uid, error = %e, "rename failed");
|
||||||
|
reply.error(Errno::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Forget the node so it re-interns under its new parent, and drop the
|
||||||
|
// destination listing so it re-enumerates on next access.
|
||||||
|
let mut st = self.state.lock().unwrap();
|
||||||
|
st.forget(&uid);
|
||||||
|
st.children.remove(&newparent);
|
||||||
|
reply.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply one remote event to the local cache and notify the kernel so it drops
|
/// Apply one remote event to the local cache and notify the kernel so it drops
|
||||||
@@ -482,8 +1032,6 @@ pub fn mount(
|
|||||||
config.mount_options = vec![
|
config.mount_options = vec![
|
||||||
MountOption::FSName("protondrive".to_string()),
|
MountOption::FSName("protondrive".to_string()),
|
||||||
MountOption::Subtype("protondrive".to_string()),
|
MountOption::Subtype("protondrive".to_string()),
|
||||||
MountOption::RO,
|
|
||||||
MountOption::NoExec,
|
|
||||||
MountOption::DefaultPermissions,
|
MountOption::DefaultPermissions,
|
||||||
];
|
];
|
||||||
info!(mountpoint = %mountpoint.display(), "mounting Proton Drive");
|
info!(mountpoint = %mountpoint.display(), "mounting Proton Drive");
|
||||||
|
|||||||
Reference in new issue
Block a user