mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-10 15:16:56 +00:00
fix(lifecycle): lifecycle fixes (#1625)
Signed-off-by: likewu <likewu@126.com> Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
+110
-131
@@ -24,6 +24,7 @@ use crate::storage::helper::OperationHelper;
|
||||
use crate::storage::options::{filter_object_metadata, get_content_sha256};
|
||||
use crate::storage::{
|
||||
access::{ReqInfo, authorize_request, has_bypass_governance_header},
|
||||
ecfs_extend::RFC1123,
|
||||
options::{
|
||||
copy_dst_opts, copy_src_opts, del_opts, extract_metadata, extract_metadata_from_mime_with_object_name,
|
||||
get_complete_multipart_upload_opts, get_opts, parse_copy_source_range, put_opts,
|
||||
@@ -93,8 +94,8 @@ use rustfs_ecstore::{
|
||||
},
|
||||
};
|
||||
use rustfs_filemeta::REPLICATE_INCOMING_DELETE;
|
||||
use rustfs_filemeta::RestoreStatusOps;
|
||||
use rustfs_filemeta::{ReplicationStatusType, ReplicationType, VersionPurgeStatusType};
|
||||
use rustfs_filemeta::{RestoreStatusOps, parse_restore_obj_status};
|
||||
use rustfs_kms::DataKey;
|
||||
use rustfs_notify::{EventArgsBuilder, notifier_global};
|
||||
use rustfs_policy::policy::{
|
||||
@@ -3665,8 +3666,44 @@ impl S3 for FS {
|
||||
{
|
||||
response.headers.insert(header_name, header_value);
|
||||
}
|
||||
|
||||
if let Some(amz_restore) = metadata_map.get(X_AMZ_RESTORE.as_str()) {
|
||||
let Ok(restore_status) = parse_restore_obj_status(amz_restore) else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrMeta".into()), "parse amz_restore failed."));
|
||||
};
|
||||
if let Ok(header_value) = HeaderValue::from_str(restore_status.to_string2().as_str()) {
|
||||
response.headers.insert(X_AMZ_RESTORE, header_value);
|
||||
}
|
||||
}
|
||||
if let Some(amz_restore_request_date) = metadata_map.get(AMZ_RESTORE_REQUEST_DATE)
|
||||
&& let Ok(header_name) = http::HeaderName::from_bytes(AMZ_RESTORE_REQUEST_DATE.as_bytes())
|
||||
{
|
||||
let Ok(amz_restore_request_date) = OffsetDateTime::parse(amz_restore_request_date, &Rfc3339) else {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrMeta".into()),
|
||||
"parse amz_restore_request_date failed.",
|
||||
));
|
||||
};
|
||||
let Ok(amz_restore_request_date) = amz_restore_request_date.format(&RFC1123) else {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrMeta".into()),
|
||||
"format amz_restore_request_date failed.",
|
||||
));
|
||||
};
|
||||
if let Ok(header_value) = HeaderValue::from_str(&amz_restore_request_date) {
|
||||
response.headers.insert(header_name, header_value);
|
||||
}
|
||||
}
|
||||
if let Some(amz_restore_expiry_days) = metadata_map.get(AMZ_RESTORE_EXPIRY_DAYS)
|
||||
&& let Ok(header_name) = http::HeaderName::from_bytes(AMZ_RESTORE_EXPIRY_DAYS.as_bytes())
|
||||
&& let Ok(header_value) = HeaderValue::from_str(amz_restore_expiry_days)
|
||||
{
|
||||
response.headers.insert(header_name, header_value);
|
||||
}
|
||||
|
||||
let result = Ok(response);
|
||||
let _ = helper.complete(&result);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -5238,6 +5275,7 @@ impl S3 for FS {
|
||||
histogram!("rustfs.object_tagging.operation.duration.seconds", "operation" => "put").record(duration.as_secs_f64());
|
||||
result
|
||||
}
|
||||
|
||||
async fn restore_object(&self, req: S3Request<RestoreObjectInput>) -> S3Result<S3Response<RestoreObjectOutput>> {
|
||||
let RestoreObjectInput {
|
||||
bucket,
|
||||
@@ -5246,76 +5284,66 @@ impl S3 for FS {
|
||||
version_id,
|
||||
..
|
||||
} = req.input.clone();
|
||||
let rreq = rreq.unwrap();
|
||||
|
||||
/*if let Err(e) = un_escape_path(object) {
|
||||
warn!("post restore object failed, e: {:?}", e);
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("PostRestoreObjectFailed".into()), "post restore object failed"));
|
||||
}*/
|
||||
let rreq = rreq.ok_or_else(|| {
|
||||
S3Error::with_message(S3ErrorCode::Custom("ErrValidRestoreObject".into()), "restore request is required")
|
||||
})?;
|
||||
|
||||
let Some(store) = new_object_layer_fn() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
|
||||
/*if Err(err) = check_request_auth_type(req, policy::RestoreObjectAction, bucket, object) {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("PostRestoreObjectFailed".into()), "post restore object failed"));
|
||||
}*/
|
||||
let version_id_str = version_id.clone().unwrap_or_default();
|
||||
let opts = post_restore_opts(&version_id_str, &bucket, &object)
|
||||
.await
|
||||
.map_err(|_| S3Error::with_message(S3ErrorCode::Custom("ErrPostRestoreOpts".into()), "restore object failed."))?;
|
||||
|
||||
/*if req.content_length <= 0 {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrEmptyRequestBody".into()), "post restore object failed"));
|
||||
}*/
|
||||
let Ok(opts) = post_restore_opts(&version_id.unwrap(), &bucket, &object).await else {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrEmptyRequestBody".into()),
|
||||
"post restore object failed",
|
||||
));
|
||||
};
|
||||
|
||||
let Ok(mut obj_info) = store.get_object_info(&bucket, &object, &opts).await else {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrEmptyRequestBody".into()),
|
||||
"post restore object failed",
|
||||
));
|
||||
};
|
||||
let mut obj_info = store
|
||||
.get_object_info(&bucket, &object, &opts)
|
||||
.await
|
||||
.map_err(|_| S3Error::with_message(S3ErrorCode::Custom("ErrInvalidObjectState".into()), "restore object failed."))?;
|
||||
|
||||
// Check if object is in a transitioned state
|
||||
if obj_info.transitioned_object.status != lifecycle::TRANSITION_COMPLETE {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrEmptyRequestBody".into()),
|
||||
"post restore object failed",
|
||||
S3ErrorCode::Custom("ErrInvalidTransitionedState".into()),
|
||||
"restore object failed.",
|
||||
));
|
||||
}
|
||||
|
||||
//let mut api_err;
|
||||
let mut _status_code = StatusCode::OK;
|
||||
let mut already_restored = false;
|
||||
if let Err(_err) = rreq.validate(store.clone()) {
|
||||
//api_err = to_api_err(ErrMalformedXML);
|
||||
//api_err.description = err.to_string();
|
||||
// Validate restore request
|
||||
if let Err(e) = rreq.validate(store.clone()) {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrEmptyRequestBody".into()),
|
||||
"post restore object failed",
|
||||
S3ErrorCode::Custom("ErrValidRestoreObject".into()),
|
||||
format!("Restore object validation failed: {}", e),
|
||||
));
|
||||
} else {
|
||||
if obj_info.restore_ongoing && (rreq.type_.is_none() || rreq.type_.as_ref().unwrap().as_str() != "SELECT") {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrObjectRestoreAlreadyInProgress".into()),
|
||||
"post restore object failed",
|
||||
));
|
||||
}
|
||||
if !obj_info.restore_ongoing && obj_info.restore_expires.unwrap().unix_timestamp() != 0 {
|
||||
_status_code = StatusCode::ACCEPTED;
|
||||
already_restored = true;
|
||||
}
|
||||
}
|
||||
let restore_expiry = lifecycle::expected_expiry_time(OffsetDateTime::now_utc(), *rreq.days.as_ref().unwrap());
|
||||
|
||||
// Check if restore is already in progress
|
||||
if obj_info.restore_ongoing && (rreq.type_.as_ref().is_none_or(|t| t.as_str() != "SELECT")) {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrObjectRestoreAlreadyInProgress".into()),
|
||||
"restore object failed.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut already_restored = false;
|
||||
if let Some(restore_expires) = obj_info.restore_expires
|
||||
&& !obj_info.restore_ongoing
|
||||
&& restore_expires.unix_timestamp() != 0
|
||||
{
|
||||
already_restored = true;
|
||||
}
|
||||
|
||||
let restore_expiry = lifecycle::expected_expiry_time(OffsetDateTime::now_utc(), *rreq.days.as_ref().unwrap_or(&1));
|
||||
let mut metadata = obj_info.user_defined.clone();
|
||||
|
||||
let mut header = HeaderMap::new();
|
||||
|
||||
let obj_info_ = obj_info.clone();
|
||||
if rreq.type_.is_none() || rreq.type_.as_ref().unwrap().as_str() != "SELECT" {
|
||||
if rreq.type_.as_ref().is_none_or(|t| t.as_str() != "SELECT") {
|
||||
obj_info.metadata_only = true;
|
||||
metadata.insert(AMZ_RESTORE_EXPIRY_DAYS.to_string(), rreq.days.unwrap().to_string());
|
||||
metadata.insert(AMZ_RESTORE_EXPIRY_DAYS.to_string(), rreq.days.unwrap_or(1).to_string());
|
||||
metadata.insert(AMZ_RESTORE_REQUEST_DATE.to_string(), OffsetDateTime::now_utc().format(&Rfc3339).unwrap());
|
||||
if already_restored {
|
||||
metadata.insert(
|
||||
@@ -5337,7 +5365,8 @@ impl S3 for FS {
|
||||
);
|
||||
}
|
||||
obj_info.user_defined = metadata;
|
||||
if let Err(_err) = store
|
||||
|
||||
store
|
||||
.clone()
|
||||
.copy_object(
|
||||
&bucket,
|
||||
@@ -5346,22 +5375,18 @@ impl S3 for FS {
|
||||
&object,
|
||||
&mut obj_info,
|
||||
&ObjectOptions {
|
||||
version_id: obj_info_.version_id.map(|e| e.to_string()),
|
||||
version_id: obj_info_.version_id.map(|v| v.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
&ObjectOptions {
|
||||
version_id: obj_info_.version_id.map(|e| e.to_string()),
|
||||
version_id: obj_info_.version_id.map(|v| v.to_string()),
|
||||
mod_time: obj_info_.mod_time,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrInvalidObjectState".into()),
|
||||
"post restore object failed",
|
||||
));
|
||||
}
|
||||
.map_err(|_| S3Error::with_message(S3ErrorCode::Custom("ErrCopyObject".into()), "restore object failed."))?;
|
||||
|
||||
if already_restored {
|
||||
let output = RestoreObjectOutput {
|
||||
request_charged: Some(RequestCharged::from_static(RequestCharged::REQUESTER)),
|
||||
@@ -5371,97 +5396,51 @@ impl S3 for FS {
|
||||
}
|
||||
}
|
||||
|
||||
let restore_object = Uuid::new_v4().to_string();
|
||||
//if let Some(rreq) = rreq {
|
||||
// Handle output location for SELECT requests
|
||||
if let Some(output_location) = &rreq.output_location
|
||||
&& let Some(s3) = &output_location.s3
|
||||
&& !s3.bucket_name.is_empty()
|
||||
{
|
||||
let restore_object = Uuid::new_v4().to_string();
|
||||
header.insert(
|
||||
X_AMZ_RESTORE_OUTPUT_PATH,
|
||||
format!("{}{}{}", s3.bucket_name, s3.prefix, restore_object).parse().unwrap(),
|
||||
);
|
||||
}
|
||||
//}
|
||||
/*send_event(EventArgs {
|
||||
event_name: event::ObjectRestorePost,
|
||||
bucket_name: bucket,
|
||||
object: obj_info,
|
||||
req_params: extract_req_params(r),
|
||||
user_agent: req.user_agent(),
|
||||
host: handlers::get_source_ip(r),
|
||||
});*/
|
||||
tokio::spawn(async move {
|
||||
/*if rreq.select_parameters.is_some() {
|
||||
let actual_size = obj_info_.get_actual_size();
|
||||
if actual_size.is_err() {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrInvalidObjectState".into()), "post restore object failed"));
|
||||
}
|
||||
|
||||
let object_rsc = s3select.new_object_read_seek_closer(
|
||||
|offset: i64| -> (ReadCloser, error) {
|
||||
rs := &HTTPRangeSpec{
|
||||
IsSuffixLength: false,
|
||||
Start: offset,
|
||||
End: -1,
|
||||
}
|
||||
return get_transitioned_object_reader(bucket, object, rs, r.Header,
|
||||
obj_info, ObjectOptions {version_id: obj_info_.version_id});
|
||||
},
|
||||
actual_size.unwrap(),
|
||||
);
|
||||
if err = rreq.select_parameters.open(object_rsc); err != nil {
|
||||
if serr, ok := err.(s3select.SelectError); ok {
|
||||
let encoded_error_response = encodeResponse(APIErrorResponse {
|
||||
code: serr.ErrorCode(),
|
||||
message: serr.ErrorMessage(),
|
||||
bucket_name: bucket,
|
||||
key: object,
|
||||
resource: r.URL.Path,
|
||||
request_id: w.Header().Get(xhttp.AmzRequestID),
|
||||
host_id: globalDeploymentID(),
|
||||
});
|
||||
//writeResponse(w, serr.HTTPStatusCode(), encodedErrorResponse, mimeXML)
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header));
|
||||
} else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrInvalidObjectState".into()), "post restore object failed"));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let nr = httptest.NewRecorder();
|
||||
let rw = xhttp.NewResponseRecorder(nr);
|
||||
rw.log_err_body = true;
|
||||
rw.log_all_body = true;
|
||||
rreq.select_parameters.evaluate(rw);
|
||||
rreq.select_parameters.Close();
|
||||
return Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header));
|
||||
}*/
|
||||
// Spawn restoration task in the background
|
||||
let store_clone = store.clone();
|
||||
let bucket_clone = bucket.clone();
|
||||
let object_clone = object.clone();
|
||||
let rreq_clone = rreq.clone();
|
||||
let version_id_clone = version_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let opts = ObjectOptions {
|
||||
transition: TransitionOptions {
|
||||
restore_request: rreq,
|
||||
restore_request: rreq_clone,
|
||||
restore_expiry,
|
||||
..Default::default()
|
||||
},
|
||||
version_id: obj_info_.version_id.map(|e| e.to_string()),
|
||||
version_id: version_id_clone,
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(err) = store.clone().restore_transitioned_object(&bucket, &object, &opts).await {
|
||||
warn!("unable to restore transitioned bucket/object {}/{}: {}", bucket, object, err.to_string());
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::Custom("ErrRestoreTransitionedObject".into()),
|
||||
format!("unable to restore transitioned bucket/object {bucket}/{object}: {err}"),
|
||||
));
|
||||
}
|
||||
|
||||
/*send_event(EventArgs {
|
||||
EventName: event.ObjectRestoreCompleted,
|
||||
BucketName: bucket,
|
||||
Object: objInfo,
|
||||
ReqParams: extractReqParams(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Host: handlers.GetSourceIP(r),
|
||||
});*/
|
||||
Ok(())
|
||||
if let Err(err) = store_clone
|
||||
.restore_transitioned_object(&bucket_clone, &object_clone, &opts)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"unable to restore transitioned bucket/object {}/{}: {}",
|
||||
bucket_clone,
|
||||
object_clone,
|
||||
err.to_string()
|
||||
);
|
||||
// Note: Errors from background tasks cannot be returned to client
|
||||
// Consider adding to monitoring/metrics system
|
||||
} else {
|
||||
info!("successfully restored transitioned object: {}/{}", bucket_clone, object_clone);
|
||||
}
|
||||
});
|
||||
|
||||
let output = RestoreObjectOutput {
|
||||
|
||||
@@ -52,7 +52,7 @@ use time::{format_description::FormatItem, macros::format_description};
|
||||
use tokio::io::AsyncRead;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
const RFC1123: &[FormatItem<'_>] =
|
||||
pub const RFC1123: &[FormatItem<'_>] =
|
||||
format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT");
|
||||
|
||||
/// =======================
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ansible-playbook rustfs/tests/lifecycle_ansible_test.yml
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
- hosts: webservers
|
||||
remote_user: leafcolor
|
||||
vars:
|
||||
ansible_ssh_pass: "123456"
|
||||
tasks:
|
||||
- name: Configure S3 bucket lifecycle
|
||||
community.aws.s3_lifecycle:
|
||||
endpoint_url: "http://www.example.com:9000"
|
||||
access_key: "rustfsadmin"
|
||||
secret_key: "rustfsadmin"
|
||||
name: "mblock"
|
||||
rule_id: "rule1"
|
||||
state: present
|
||||
expiration_days: "1"
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright 2024 RustFS Team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from datetime import timedelta
|
||||
import uuid
|
||||
from fastapi import UploadFile
|
||||
from minio import Minio
|
||||
from minio.commonconfig import Filter
|
||||
from minio.error import S3Error
|
||||
from minio.lifecycleconfig import Expiration, LifecycleConfig, Rule
|
||||
import os
|
||||
from collections import namedtuple
|
||||
|
||||
Settings = namedtuple("Settings", ['oss_endpoint',
|
||||
'oss_access_key',
|
||||
'oss_secret_key',
|
||||
'oss_bucket_name',
|
||||
'oss_lifecycle_days',
|
||||
'oss_secure',
|
||||
'oss_region'])
|
||||
settings = Settings(
|
||||
oss_endpoint=os.getenv("TEST_RUSTFS_SERVER", "localhost:9000"),
|
||||
oss_access_key = "rustfsadmin",
|
||||
oss_secret_key = "rustfsadmin",
|
||||
oss_bucket_name = "mblock99",
|
||||
oss_lifecycle_days = 1,
|
||||
oss_secure = False,
|
||||
oss_region = ""
|
||||
)
|
||||
|
||||
class OSS:
|
||||
def __init__(self):
|
||||
self.bucket_name = settings.oss_bucket_name
|
||||
self.lifecycle_days = settings.oss_lifecycle_days
|
||||
self.client = Minio(
|
||||
endpoint=settings.oss_endpoint,
|
||||
access_key=settings.oss_access_key,
|
||||
secret_key=settings.oss_secret_key,
|
||||
secure=settings.oss_secure,
|
||||
region=settings.oss_region,
|
||||
)
|
||||
|
||||
def new_uuid(self):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def create_bucket(self):
|
||||
found = self.client.bucket_exists(self.bucket_name)
|
||||
if not found:
|
||||
try:
|
||||
self.client.make_bucket(self.bucket_name)
|
||||
self.set_lifecycle_expiration(days=self.lifecycle_days)
|
||||
print(f"Bucket {self.bucket_name} created successfully")
|
||||
except S3Error as exc:
|
||||
if exc.code == "BucketAlreadyOwnedByYou":
|
||||
print(f"Bucket {self.bucket_name} already owned by you; continuing")
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
print(f"Bucket {self.bucket_name} already exists")
|
||||
|
||||
def set_lifecycle_expiration(self,days: int = 1, prefix: str = "") -> None:
|
||||
"""
|
||||
设置按天自动过期删除(MinIO 按天、每天巡检一次;<1天不生效)
|
||||
"""
|
||||
rule_filter = Filter(prefix=prefix or "")
|
||||
rule = Rule(
|
||||
rule_id=f"expire-{prefix or 'all'}-{days}d",
|
||||
status="Enabled",
|
||||
rule_filter=rule_filter,
|
||||
expiration=Expiration(days=int(days)),
|
||||
)
|
||||
cfg = LifecycleConfig([rule])
|
||||
self.client.set_bucket_lifecycle(self.bucket_name, cfg)
|
||||
|
||||
def upload_file(self, file: UploadFile):
|
||||
"""
|
||||
上传文件到OSS,返回文件的UUID
|
||||
"""
|
||||
ext = os.path.splitext(file.filename)[1]
|
||||
uuid = self.new_uuid()
|
||||
filename = f'{uuid}{ext}'
|
||||
file.file.seek(0)
|
||||
self.client.put_object(
|
||||
self.bucket_name,
|
||||
filename,
|
||||
file.file,
|
||||
length=-1,
|
||||
part_size=10*1024*1024,
|
||||
)
|
||||
return filename
|
||||
|
||||
def get_presigned_url(self, filename: str):
|
||||
"""
|
||||
获取文件的预签名URL,用于下载文件
|
||||
"""
|
||||
return self.client.presigned_get_object(
|
||||
self.bucket_name, filename, expires=timedelta(days=self.lifecycle_days)
|
||||
)
|
||||
|
||||
|
||||
def get_oss():
|
||||
"""
|
||||
获取OSS实例
|
||||
"""
|
||||
return OSS()
|
||||
|
||||
if __name__ == "__main__":
|
||||
oss = get_oss()
|
||||
oss.create_bucket()
|
||||
Reference in New Issue
Block a user