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",
"env_logger",
"futures-util",
"libc",
"log",
"olivefs_common",
"path-absolutize",

View File

@ -47,6 +47,13 @@ fn test_bits(haystack: i32, needle: i32) -> bool {
(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 {
/// Initialize filesystem.
/// Called before any other filesystem method.
@ -164,8 +171,8 @@ impl Filesystem for OliveFilesystem {
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
_atime: Option<TimeOrNow>,
_mtime: Option<TimeOrNow>,
atime: Option<TimeOrNow>,
mtime: Option<TimeOrNow>,
_ctime: Option<SystemTime>,
fh: Option<u64>,
_crtime: Option<SystemTime>,
@ -174,12 +181,44 @@ impl Filesystem for OliveFilesystem {
flags: Option<u32>,
reply: ReplyAttr,
) {
debug!(
"[Not Implemented] setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \
gid: {:?}, size: {:?}, fh: {:?}, flags: {:?})",
ino, mode, uid, gid, size, fh, flags
let requester = self.requester.clone();
self.spawn_with_error_handler(
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.

View File

@ -12,7 +12,7 @@ use std::net::SocketAddr;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use std::time::{Duration, SystemTime};
use crate::configuration::StreamingReaderConfig;
use crate::requester::streaming_reader::StreamingReader;
@ -269,6 +269,29 @@ impl Requester {
.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(
&self,
file_handle: u32,

View File

@ -79,6 +79,15 @@ pub enum DataCommand {
FlushFile {
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 {}

View File

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

View File

@ -94,6 +94,24 @@ pub async fn handle_command_stream(
)
.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::{EBADFD, EFAULT, ENOENT};
use olivefs_common::error_codes::{EBADFD, EFAULT, ENOENT, ENOSYS};
use olivefs_common::messages::{
DataResponse, DirectoryEntry, FileKind, FileMetadata, OpenMode, VnodeId,
};
@ -7,18 +7,23 @@ use path_absolutize::Absolutize;
use slab::Slab;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::io;
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::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use log::{error, warn};
use std::time::{Duration, Instant, SystemTime};
use tokio::fs::{OpenOptions, ReadDir};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
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
#[derive(Default)]
pub struct ServerwideState {
@ -109,6 +114,7 @@ impl FileAccess {
if let Some(inode_info) = inode_map.get(vnode.0 as usize) {
Ok(inode_info.real_path.clone())
} else {
// TODO just make this an io::Error.
Err(DataResponse::Error {
code: error_codes::EFAULT,
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> {
let paths_to_vnodes = self.client_state.existing_paths_to_inodes.read().await;
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.
let mut open_options = OpenOptions::new();
// IMPORTANT: Don't follow symlinks
open_options.custom_flags(O_NOFOLLOW);
// Only create new files
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))
}
}