mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-07 13:53:12 +00:00
fe5690ca70
Co-authored-by: houseme <housemecn@gmail.com>
246 lines
10 KiB
Rust
246 lines
10 KiB
Rust
// 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.
|
|
|
|
//! Per-session state types for the SFTP driver.
|
|
//!
|
|
//! Operation implementations are defined in the relevant modules
|
|
//! (attrs.rs, read.rs, write.rs, dir.rs, driver.rs). state.rs holds
|
|
//! only type definitions and associated state definitions.
|
|
|
|
use super::read_cache::ReadCache;
|
|
use russh_sftp::protocol::FileAttributes;
|
|
use s3s::dto::ETag;
|
|
|
|
/// State held per open handle.
|
|
///
|
|
/// File handles cache the object size so READ can detect end-of-file without
|
|
/// re-issuing HeadObject on every call. Directory handles carry the S3
|
|
/// continuation token so each READDIR response corresponds to one S3
|
|
/// ListObjectsV2 page. This bounds response size without imposing an
|
|
/// arbitrary batch limit. Write handles run a multipart state machine:
|
|
/// small files buffer in memory and upload via a single PutObject at CLOSE,
|
|
/// large files transition to streaming multipart uploads as the buffer fills.
|
|
/// See WritePhase for the full state machine.
|
|
pub(super) enum HandleState {
|
|
File {
|
|
bucket: String,
|
|
key: String,
|
|
/// Object size captured at OPEN time. READ uses this to return EOF
|
|
/// once the offset reaches or exceeds the end of the object, as
|
|
/// required by SFTPv3 draft section 6.4.
|
|
size: u64,
|
|
/// Attributes captured at OPEN time so FSTAT can answer without a
|
|
/// second HeadObject.
|
|
attrs: FileAttributes,
|
|
/// Per-handle cached chunk of bytes fetched on the previous
|
|
/// READ miss. FXP_READs whose target range sits inside the
|
|
/// cached chunk are served from the buffer without a backend
|
|
/// round trip. Constructed empty in open_read. Dropped when
|
|
/// CLOSE removes the handle from the table, or when the
|
|
/// SftpDriver Drop impl runs at session teardown.
|
|
read_cache: ReadCache,
|
|
},
|
|
Dir(DirCursor),
|
|
Write {
|
|
bucket: String,
|
|
key: String,
|
|
/// Attributes returned by FSTAT against this handle.
|
|
/// The size field tracks the running total of bytes received so
|
|
/// a client polling FSTAT during a transfer sees the progress.
|
|
attrs: FileAttributes,
|
|
/// Raw attributes supplied on OPEN. Only fields explicitly set
|
|
/// by the client are copied into S3 user metadata at object
|
|
/// creation time.
|
|
open_attrs: FileAttributes,
|
|
/// Multipart upload lifecycle state. See WritePhase.
|
|
phase: WritePhase,
|
|
},
|
|
}
|
|
|
|
/// Write-side state machine for a single open write handle.
|
|
///
|
|
/// Transitions are strictly forward. A handle begins in Buffering. Once the
|
|
/// first full part is ready, the driver issues CreateMultipartUpload and
|
|
/// transitions to Streaming. On any UploadPart failure the phase moves to
|
|
/// Failed, which rejects further writes and releases the upload_id via
|
|
/// AbortMultipartUpload at CLOSE. There is no recovery from Failed.
|
|
///
|
|
///
|
|
/// OPEN
|
|
/// |
|
|
/// v
|
|
/// Buffering --CLOSE--> PutObject (small file) ---------> DONE
|
|
/// |
|
|
/// | buffer >= part_size
|
|
/// | CreateMultipartUpload ok
|
|
/// v
|
|
/// Streaming --CLOSE--> UploadPart (tail) then
|
|
/// | ^ CompleteMultipartUpload --------> DONE
|
|
/// | | (large file)
|
|
/// | |
|
|
/// | | buffer >= part_size
|
|
/// | | UploadPart ok (loop)
|
|
/// | |
|
|
/// | UploadPart fails
|
|
/// v
|
|
/// Failed --CLOSE--> AbortMultipartUpload ---> (handle gone, no object)
|
|
///
|
|
/// Retry: CreateMultipartUpload fails -> stay in Buffering,
|
|
/// retry on next flush.
|
|
///
|
|
pub(super) enum WritePhase {
|
|
/// No multipart upload has been started. Bytes accumulate in part_buffer.
|
|
/// On CLOSE the buffered bytes upload via a single PutObject. If
|
|
/// CreateMultipartUpload fails on the first full-part flush, the phase
|
|
/// stays in Buffering and the next full-part flush retries the call:
|
|
/// a transient S3 error is invisible to the client.
|
|
Buffering {
|
|
/// Bytes received via WRITE not yet flushed to S3. Bounded by
|
|
/// part_size: the while-loop in write() drains it below part_size
|
|
/// before returning.
|
|
part_buffer: Vec<u8>,
|
|
},
|
|
/// CreateMultipartUpload has been issued. Full parts flush at the
|
|
/// part_size boundary. On CLOSE, the final partial part is uploaded
|
|
/// via UploadPart and the upload is finalised via
|
|
/// CompleteMultipartUpload.
|
|
Streaming {
|
|
/// upload_id returned by CreateMultipartUpload. Required by every
|
|
/// subsequent UploadPart, CompleteMultipartUpload, and
|
|
/// AbortMultipartUpload call.
|
|
upload_id: String,
|
|
/// Cached result of authorize_operation for AbortMultipartUpload,
|
|
/// evaluated at CreateMultipartUpload time. Drop consults this
|
|
/// to decide whether to issue AbortMultipartUpload without
|
|
/// running an async auth call (Drop is synchronous). close()
|
|
/// consults it too for consistency: same policy decision, same
|
|
/// observable outcome. False means the principal's IAM policy
|
|
/// denies AbortMultipartUpload, so cleanup is deferred to the
|
|
/// bucket's AbortIncompleteMultipartUpload lifecycle rule. The
|
|
/// flag is cached for one upload's lifetime: a policy edit
|
|
/// between the cache and the abort attempt is not honoured in
|
|
/// this session.
|
|
abort_authorized: bool,
|
|
/// Bytes received via WRITE not yet flushed to S3.
|
|
part_buffer: Vec<u8>,
|
|
/// Parts already uploaded. Passed to CompleteMultipartUpload in
|
|
/// order. Each entry carries the part number and the ETag returned
|
|
/// by UploadPart.
|
|
uploaded_parts: Vec<CompletedPart>,
|
|
/// Part number to use for the next UploadPart call. S3 part numbers
|
|
/// begin at 1 and increase monotonically.
|
|
next_part_number: i32,
|
|
},
|
|
/// An UploadPart call failed. The upload_id is retained so close()
|
|
/// can call AbortMultipartUpload when policy permits. Further
|
|
/// writes are rejected.
|
|
Failed {
|
|
/// upload_id returned by the CreateMultipartUpload call that opened
|
|
/// the now-failed upload.
|
|
upload_id: String,
|
|
/// Carried forward from Streaming at the point of failure. See
|
|
/// the identically named field on Streaming for the contract.
|
|
abort_authorized: bool,
|
|
},
|
|
}
|
|
|
|
/// Record of one successfully uploaded part. Carries the part number and
|
|
/// ETag needed by CompleteMultipartUpload to assemble the final object.
|
|
#[derive(Clone)]
|
|
pub(super) struct CompletedPart {
|
|
pub(super) part_number: i32,
|
|
pub(super) e_tag: ETag,
|
|
}
|
|
|
|
/// Identifier plus cached abort authorisation for one S3 multipart
|
|
/// upload. Holds the upload_id and the result of the AbortMultipartUpload
|
|
/// IAM probe issued at CreateMultipartUpload time. Holding the two
|
|
/// fields together prevents drift: any code path with the upload_id
|
|
/// also has the abort decision in scope without re-probing IAM, and the
|
|
/// synchronous Drop on SftpDriver can honour a Deny-Abort policy from
|
|
/// the cached flag without an async call.
|
|
///
|
|
/// Cloneable so a tombstone copy can live in the handle table while a
|
|
/// write_dispatch await holds a working copy. The fields are one String
|
|
/// and one bool, so cloning is cheap.
|
|
#[derive(Clone, Debug)]
|
|
pub(super) struct MultipartUpload {
|
|
pub(super) upload_id: String,
|
|
pub(super) abort_authorized: bool,
|
|
}
|
|
|
|
/// Directory iteration state.
|
|
///
|
|
/// Root lists buckets. ListBuckets is not batched: one response carries
|
|
/// every bucket the principal can see. Bucket and prefix listings walk
|
|
/// ListObjectsV2 one batch at a time, using continuation_token to cross
|
|
/// batch boundaries. The dots_emitted flag ensures the conventional "."
|
|
/// and ".." entries are produced exactly once, on the first READDIR call.
|
|
///
|
|
/// Clone is derived so the READDIR handler can install a cancellation-safety
|
|
/// tombstone (the pre-advance cursor) in the handle table before the
|
|
/// list_objects_v2 await. A cancelled READDIR leaves the tombstone so the
|
|
/// client's next READDIR resumes from the un-advanced position.
|
|
#[derive(Clone)]
|
|
pub(super) enum DirCursor {
|
|
Root {
|
|
buckets_delivered: bool,
|
|
dots_emitted: bool,
|
|
},
|
|
Listing {
|
|
bucket: String,
|
|
/// Object prefix terminated by "/", or empty when listing the root
|
|
/// of a bucket. S3 list_objects_v2 with a trailing-slash prefix
|
|
/// returns entries immediately under the prefix.
|
|
prefix: String,
|
|
/// Position in the ListObjectsV2 batch walk. Initial before the
|
|
/// first batch, Next(token) between batches, Done once S3 reports
|
|
/// the listing is exhausted.
|
|
continuation: ListingContinuation,
|
|
dots_emitted: bool,
|
|
},
|
|
}
|
|
|
|
/// Position within a batched S3 ListObjectsV2 walk. The state machine is
|
|
/// total: every transition arrives at exactly one of these variants.
|
|
/// Initial means no batch has been fetched and the next call to
|
|
/// next_listing_page issues list_objects_v2 with no continuation_token.
|
|
/// Next(token) means a previous batch returned this continuation token
|
|
/// and the next call passes it to list_objects_v2 to fetch the following
|
|
/// batch. Done means the listing is exhausted and subsequent calls
|
|
/// return an empty Vec without a network round trip.
|
|
#[derive(Clone)]
|
|
pub(super) enum ListingContinuation {
|
|
Initial,
|
|
Next(String),
|
|
Done,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::super::constants::limits::{S3_COPY_OBJECT_MAX_SIZE, S3_MAX_MULTIPART_PARTS, S3_MAX_PART_SIZE, S3_MIN_PART_SIZE};
|
|
|
|
#[test]
|
|
fn multipart_constants_match_s3_limits() {
|
|
// S3_COPY_OBJECT_MAX_SIZE 5 GiB is the CopyObject single-shot ceiling.
|
|
// S3_MIN_PART_SIZE 5 MiB is the S3 minimum for non-final parts.
|
|
// S3_MAX_PART_SIZE 5 GiB is the S3 maximum for any single part.
|
|
// S3_MAX_MULTIPART_PARTS 10000 is the S3 cap on parts per upload.
|
|
assert_eq!(S3_COPY_OBJECT_MAX_SIZE, 5 * 1024 * 1024 * 1024);
|
|
assert_eq!(S3_MIN_PART_SIZE, 5 * 1024 * 1024);
|
|
assert_eq!(S3_MAX_PART_SIZE, 5 * 1024 * 1024 * 1024);
|
|
assert_eq!(S3_MAX_MULTIPART_PARTS, 10_000);
|
|
}
|
|
}
|