Support setattr (partially; only for truncation)
Some checks are pending
continuous-integration/drone the build is running
Some checks are pending
continuous-integration/drone the build is running
This commit is contained in:
parent
0c893c02d4
commit
39df58851d
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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 {}
|
||||||
|
@ -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" }
|
||||||
|
@ -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?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user