Support setattr (partially; only for truncation)
Some checks are pending
continuous-integration/drone the build is running

This commit is contained in:
Olivier 'reivilibre' 2022-01-24 21:25:53 +00:00
parent 0c893c02d4
commit 39df58851d
7 changed files with 186 additions and 12 deletions

1
Cargo.lock generated
View File

@ -889,6 +889,7 @@ dependencies = [
"dashmap 5.0.0", "dashmap 5.0.0",
"env_logger", "env_logger",
"futures-util", "futures-util",
"libc",
"log", "log",
"olivefs_common", "olivefs_common",
"path-absolutize", "path-absolutize",

View File

@ -47,6 +47,13 @@ fn test_bits(haystack: i32, needle: i32) -> bool {
(haystack & needle) == needle (haystack & needle) == needle
} }
fn absolutise_time_or_now(input: TimeOrNow) -> SystemTime {
match input {
TimeOrNow::SpecificTime(time) => time,
TimeOrNow::Now => SystemTime::now(),
}
}
impl Filesystem for OliveFilesystem { impl Filesystem for OliveFilesystem {
/// Initialize filesystem. /// Initialize filesystem.
/// Called before any other filesystem method. /// Called before any other filesystem method.
@ -164,8 +171,8 @@ impl Filesystem for OliveFilesystem {
uid: Option<u32>, uid: Option<u32>,
gid: Option<u32>, gid: Option<u32>,
size: Option<u64>, size: Option<u64>,
_atime: Option<TimeOrNow>, atime: Option<TimeOrNow>,
_mtime: Option<TimeOrNow>, mtime: Option<TimeOrNow>,
_ctime: Option<SystemTime>, _ctime: Option<SystemTime>,
fh: Option<u64>, fh: Option<u64>,
_crtime: Option<SystemTime>, _crtime: Option<SystemTime>,
@ -174,12 +181,44 @@ impl Filesystem for OliveFilesystem {
flags: Option<u32>, flags: Option<u32>,
reply: ReplyAttr, reply: ReplyAttr,
) { ) {
debug!( let requester = self.requester.clone();
"[Not Implemented] setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \
gid: {:?}, size: {:?}, fh: {:?}, flags: {:?})", self.spawn_with_error_handler(
ino, mode, uid, gid, size, fh, flags async move {
let vnode = VnodeId(
ino.try_into()
.context("Converting u64 inode to u32 VnodeId.")?,
);
match requester
.setattr(
vnode,
mode,
uid,
gid,
size,
atime.map(absolutise_time_or_now),
mtime.map(absolutise_time_or_now),
)
.await?
{
DataResponse::Success(file_metadata) => {
reply.attr(&Duration::from_secs(5), &file_metadata.into());
}
DataResponse::Error { code, message } => {
warn!(
"setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \
gid: {:?}, size: {:?}, fh: {:?}, flags: {:?}): {:?}",
ino, mode, uid, gid, size, fh, flags, message
);
reply.error(code as c_int);
}
}
Ok(())
},
"setattr",
); );
reply.error(ENOSYS);
} }
/// Read symbolic link. /// Read symbolic link.

View File

@ -12,7 +12,7 @@ use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::{Duration, SystemTime};
use crate::configuration::StreamingReaderConfig; use crate::configuration::StreamingReaderConfig;
use crate::requester::streaming_reader::StreamingReader; use crate::requester::streaming_reader::StreamingReader;
@ -269,6 +269,29 @@ impl Requester {
.await .await
} }
pub async fn setattr(
&self,
vnode: VnodeId,
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<SystemTime>,
mtime: Option<SystemTime>,
) -> anyhow::Result<DataResponse<FileMetadata>> {
self.internal
.command(&DataCommand::SetAttr {
vnode,
mode,
uid,
gid,
size,
atime,
mtime,
})
.await
}
pub async fn start_streaming_reader( pub async fn start_streaming_reader(
&self, &self,
file_handle: u32, file_handle: u32,

View File

@ -79,6 +79,15 @@ pub enum DataCommand {
FlushFile { FlushFile {
file_handle: u32, file_handle: u32,
}, },
SetAttr {
vnode: VnodeId,
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<SystemTime>,
mtime: Option<SystemTime>,
},
} }
pub trait DataResponseBase: Serialize + DeserializeOwned + Debug + Clone + 'static {} pub trait DataResponseBase: Serialize + DeserializeOwned + Debug + Clone + 'static {}

View File

@ -39,5 +39,8 @@ x509-parser = "0.12.0"
# consider also: sharded-slab (concurrent access), thunderdome (generational indices) # consider also: sharded-slab (concurrent access), thunderdome (generational indices)
slab = "0.4.5" slab = "0.4.5"
## System
libc = "0.2.112"
## Common ## Common
olivefs_common = { path = "../olivefs_common" } olivefs_common = { path = "../olivefs_common" }

View File

@ -94,6 +94,24 @@ pub async fn handle_command_stream(
) )
.await?; .await?;
} }
DataCommand::SetAttr {
vnode,
mode,
uid,
gid,
size,
atime,
mtime,
} => {
send_bare_message(
&mut tx,
&file_access
.setattr(vnode, mode, uid, gid, size, atime, mtime)
.await
.unwrap_or_else(error_to_response),
)
.await?;
}
} }
} }

View File

@ -1,5 +1,5 @@
use olivefs_common::error_codes; use olivefs_common::error_codes;
use olivefs_common::error_codes::{EBADFD, EFAULT, ENOENT}; use olivefs_common::error_codes::{EBADFD, EFAULT, ENOENT, ENOSYS};
use olivefs_common::messages::{ use olivefs_common::messages::{
DataResponse, DirectoryEntry, FileKind, FileMetadata, OpenMode, VnodeId, DataResponse, DirectoryEntry, FileKind, FileMetadata, OpenMode, VnodeId,
}; };
@ -7,18 +7,23 @@ use path_absolutize::Absolutize;
use slab::Slab; use slab::Slab;
use std::borrow::Borrow; use std::borrow::Borrow;
use std::collections::HashMap; use std::collections::HashMap;
use std::io;
use std::io::SeekFrom; use std::io::SeekFrom;
use anyhow::bail; use anyhow::{anyhow, bail};
use libc::{ELOOP, O_NOFOLLOW};
use log::{error, warn};
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant, SystemTime};
use log::{error, warn};
use tokio::fs::{OpenOptions, ReadDir}; use tokio::fs::{OpenOptions, ReadDir};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use tokio::sync::RwLock; use tokio::sync::RwLock;
// 4 symlinks ought to be enough for anybody!
pub const MAX_SYMLINK_RESOLVE_TRIES: u32 = 4;
/// Server-wide state that all clients might need to mess with /// Server-wide state that all clients might need to mess with
#[derive(Default)] #[derive(Default)]
pub struct ServerwideState { pub struct ServerwideState {
@ -109,6 +114,7 @@ impl FileAccess {
if let Some(inode_info) = inode_map.get(vnode.0 as usize) { if let Some(inode_info) = inode_map.get(vnode.0 as usize) {
Ok(inode_info.real_path.clone()) Ok(inode_info.real_path.clone())
} else { } else {
// TODO just make this an io::Error.
Err(DataResponse::Error { Err(DataResponse::Error {
code: error_codes::EFAULT, code: error_codes::EFAULT,
message: "No inode info".to_string(), message: "No inode info".to_string(),
@ -116,6 +122,45 @@ impl FileAccess {
} }
} }
async fn resolve_vnode_including_follow_symlinks_safely_best_effort(
&self,
vnode: VnodeId,
) -> anyhow::Result<PathBuf> {
let real_path = self
.resolve_vnode::<()>(vnode)
.await
.map_err(|_| anyhow!("No inode info"))?;
Ok(self.safely_resolve_symlinks_best_effort(real_path).await?)
}
/// Guarantees that the end file was not a symlink at the time of checking.
/// The caller must still prevent themselves from opening symlinks (but they may safely fail
/// on opening a symlink if it gets changed under their feet).
/// The symlinks will stop resolving if they ever point outside of the root directory, even
/// if they point to a symlink that points back into the restricted tree.
async fn safely_resolve_symlinks_best_effort(
&self,
path: PathBuf,
) -> Result<PathBuf, io::Error> {
let mut path = path;
let root = self.client_info.root.clone();
tokio::task::spawn_blocking(move || -> Result<PathBuf, io::Error> {
for _ in 0..MAX_SYMLINK_RESOLVE_TRIES {
let abs_path = &path.absolutize_virtually(&root)?;
let metadata = abs_path.symlink_metadata()?;
if !metadata.file_type().is_symlink() {
return Ok(abs_path.to_path_buf());
}
path = abs_path.read_link()?;
}
Err(io::Error::from_raw_os_error(ELOOP)).into()
})
.await
.map_err(|_| io::Error::from_raw_os_error(EFAULT))?
}
async fn allocate_vnode(&self, path: PathBuf) -> anyhow::Result<VnodeId> { async fn allocate_vnode(&self, path: PathBuf) -> anyhow::Result<VnodeId> {
let paths_to_vnodes = self.client_state.existing_paths_to_inodes.read().await; let paths_to_vnodes = self.client_state.existing_paths_to_inodes.read().await;
if let Some(vi) = paths_to_vnodes.get(&path) { if let Some(vi) = paths_to_vnodes.get(&path) {
@ -314,6 +359,9 @@ impl FileAccess {
// We'll allocate the Vnode after we know the target doesn't already exist. Fill in 0 for now. // We'll allocate the Vnode after we know the target doesn't already exist. Fill in 0 for now.
let mut open_options = OpenOptions::new(); let mut open_options = OpenOptions::new();
// IMPORTANT: Don't follow symlinks
open_options.custom_flags(O_NOFOLLOW);
// Only create new files // Only create new files
open_options.create_new(true); open_options.create_new(true);
@ -580,4 +628,37 @@ impl FileAccess {
}), }),
} }
} }
pub async fn setattr(
&self,
vnode: VnodeId,
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<SystemTime>,
mtime: Option<SystemTime>,
) -> anyhow::Result<DataResponse<FileMetadata>> {
if mode.is_some() || uid.is_some() || gid.is_some() || atime.is_some() || mtime.is_some() {
return Err(io::Error::from_raw_os_error(ENOSYS).into());
}
let path = self
.resolve_vnode_including_follow_symlinks_safely_best_effort(vnode)
.await?;
if let Some(truncate_to) = size {
let mut open_options = OpenOptions::new();
// IMPORTANT: Don't follow symlinks
open_options.custom_flags(O_NOFOLLOW);
open_options.write(true);
let file = open_options.open(&path).await?;
file.set_len(truncate_to).await?;
}
let metadata = self.read_metadata(&path, vnode).await?;
Ok(DataResponse::Success(metadata))
}
} }