mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-29 09:38:59 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9862677fcf | |||
| e50bc4c60c | |||
| 5f6104731d | |||
| 6a6866c337 | |||
| ce2ce4b16e | |||
| 1ecd5a87d9 | |||
| 72aead5466 | |||
| abd5dff9b5 | |||
| 040b05c318 | |||
| ce470c95c4 | |||
| 32e531bc61 | |||
| dcf25e46af | |||
| 2b079ae065 | |||
| d41ccc1551 | |||
| fa17f7b1e3 | |||
| c41299a29f | |||
| 79156d2d82 | |||
| 26542b741e | |||
| 8b2b4a0146 | |||
| 5cf9087113 | |||
| dd12250987 | |||
| e172b277f2 | |||
| 086331b8e7 | |||
| 96d22c3276 | |||
| caa3564439 | |||
| 18933fdb58 | |||
| 65a731a243 | |||
| 89035d3b3b | |||
| c6527643a3 | |||
| b9157d5e9d | |||
| 20be2d9859 | |||
| 855541678e | |||
| 73d3d8ab5c | |||
| 6983a3ffce | |||
| d6653f1258 | |||
| 7ab53a6d7d | |||
| 85ee9811d8 | |||
| 61bd76f77e | |||
| 8cf611426b | |||
| b0ac977a3d |
@@ -12,56 +12,96 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: "setup"
|
||||
|
||||
description: "setup environment for rustfs"
|
||||
name: "Setup Rust Environment"
|
||||
description: "Setup Rust development environment with caching for RustFS"
|
||||
|
||||
inputs:
|
||||
rust-version:
|
||||
required: true
|
||||
description: "Rust version to install"
|
||||
required: false
|
||||
default: "stable"
|
||||
description: "Rust version to use"
|
||||
cache-shared-key:
|
||||
required: true
|
||||
default: ""
|
||||
description: "Cache key for shared cache"
|
||||
description: "Shared cache key for Rust dependencies"
|
||||
required: false
|
||||
default: "rustfs-deps"
|
||||
cache-save-if:
|
||||
required: true
|
||||
default: ${{ github.ref == 'refs/heads/main' }}
|
||||
description: "Cache save condition"
|
||||
runs-on:
|
||||
required: true
|
||||
default: "ubuntu-latest"
|
||||
description: "Running system"
|
||||
description: "Condition for saving cache"
|
||||
required: false
|
||||
default: "true"
|
||||
install-cross-tools:
|
||||
description: "Install cross-compilation tools"
|
||||
required: false
|
||||
default: "false"
|
||||
target:
|
||||
description: "Target architecture to add"
|
||||
required: false
|
||||
default: ""
|
||||
github-token:
|
||||
description: "GitHub token for API access"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
if: inputs.runs-on == 'ubuntu-latest'
|
||||
- name: Install system dependencies (Ubuntu)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y musl-tools build-essential lld libdbus-1-dev libwayland-dev libwebkit2gtk-4.1-dev libxdo-dev
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
musl-tools \
|
||||
build-essential \
|
||||
lld \
|
||||
libdbus-1-dev \
|
||||
libwayland-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libxdo-dev \
|
||||
pkg-config \
|
||||
libssl-dev
|
||||
|
||||
- uses: arduino/setup-protoc@v3
|
||||
- name: Cache protoc binary
|
||||
id: cache-protoc
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.local/bin/protoc
|
||||
key: protoc-31.1-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Install protoc
|
||||
if: steps.cache-protoc.outputs.cache-hit != 'true'
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: "31.1"
|
||||
github-token: ${{ inputs.github-token }}
|
||||
|
||||
- uses: Nugine/setup-flatc@v1
|
||||
- name: Install flatc
|
||||
uses: Nugine/setup-flatc@v1
|
||||
with:
|
||||
version: "25.2.10"
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ inputs.rust-version }}
|
||||
targets: ${{ inputs.target }}
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install Zig
|
||||
if: inputs.install-cross-tools == 'true'
|
||||
uses: mlugg/setup-zig@v2
|
||||
|
||||
- name: Install cargo-zigbuild
|
||||
if: inputs.install-cross-tools == 'true'
|
||||
uses: taiki-e/install-action@cargo-zigbuild
|
||||
|
||||
- name: Setup Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
shared-key: ${{ inputs.cache-shared-key }}
|
||||
save-if: ${{ inputs.cache-save-if }}
|
||||
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: taiki-e/install-action@cargo-zigbuild
|
||||
# Cache workspace dependencies
|
||||
workspaces: |
|
||||
. -> target
|
||||
cli/rustfs-gui -> cli/rustfs-gui/target
|
||||
|
||||
+49
-10
@@ -12,28 +12,67 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: Audit
|
||||
name: Security Audit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '.github/workflows/audit.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '.github/workflows/audit.yml'
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # at midnight of each sunday
|
||||
- cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
security-audit:
|
||||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: taiki-e/install-action@cargo-audit
|
||||
- run: cargo audit -D warnings
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install cargo-audit
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-audit
|
||||
|
||||
- name: Run security audit
|
||||
run: |
|
||||
cargo audit -D warnings --json | tee audit-results.json
|
||||
|
||||
- name: Upload audit results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: security-audit-results-${{ github.run_number }}
|
||||
path: audit-results.json
|
||||
retention-days: 30
|
||||
|
||||
dependency-review:
|
||||
name: Dependency Review
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
comment-summary-in-pr: true
|
||||
|
||||
+300
-519
@@ -12,572 +12,353 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: Build RustFS And GUI
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # at midnight of each sunday
|
||||
push:
|
||||
tags: ["v*", "*"]
|
||||
branches:
|
||||
- main
|
||||
tags: ["v*"]
|
||||
branches: [main]
|
||||
paths:
|
||||
- "rustfs/**"
|
||||
- "cli/**"
|
||||
- "crates/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".github/workflows/build.yml"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "rustfs/**"
|
||||
- "cli/**"
|
||||
- "crates/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".github/workflows/build.yml"
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_build:
|
||||
description: "Force build even without changes"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
# Optimize build performance
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUSTFLAGS: "-C target-cpu=native"
|
||||
|
||||
jobs:
|
||||
# First layer: GitHub Actions level optimization (handling duplicates and concurrency)
|
||||
skip-duplicate:
|
||||
name: Skip Duplicate Actions
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
steps:
|
||||
- name: Skip duplicate actions
|
||||
id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
cancel_others: true
|
||||
paths_ignore: '["*.md", "docs/**", "deploy/**", "scripts/dev_*.sh"]'
|
||||
|
||||
# Second layer: Business logic level checks (handling build strategy)
|
||||
build-check:
|
||||
name: Build Strategy Check
|
||||
needs: skip-duplicate
|
||||
if: needs.skip-duplicate.outputs.should_skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_build: ${{ steps.check.outputs.should_build }}
|
||||
build_type: ${{ steps.check.outputs.build_type }}
|
||||
steps:
|
||||
- name: Determine build strategy
|
||||
id: check
|
||||
run: |
|
||||
should_build=false
|
||||
build_type="none"
|
||||
|
||||
# Business logic: when we need to build
|
||||
if [[ "${{ github.event_name }}" == "schedule" ]] || \
|
||||
[[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \
|
||||
[[ "${{ github.event.inputs.force_build }}" == "true" ]] || \
|
||||
[[ "${{ contains(github.event.head_commit.message, '--build') }}" == "true" ]]; then
|
||||
should_build=true
|
||||
build_type="development"
|
||||
fi
|
||||
|
||||
if [[ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then
|
||||
should_build=true
|
||||
build_type="release"
|
||||
fi
|
||||
|
||||
echo "should_build=$should_build" >> $GITHUB_OUTPUT
|
||||
echo "build_type=$build_type" >> $GITHUB_OUTPUT
|
||||
echo "Build needed: $should_build (type: $build_type)"
|
||||
|
||||
# Build RustFS binaries
|
||||
build-rustfs:
|
||||
name: Build RustFS
|
||||
needs: [skip-duplicate, build-check]
|
||||
if: needs.skip-duplicate.outputs.should_skip != 'true' && needs.build-check.outputs.should_build == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
# Only execute in the following cases: 1) tag push 2) scheduled run 3) commit message contains --build
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/') ||
|
||||
github.event_name == 'schedule' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
contains(github.event.head_commit.message, '--build')
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
variant:
|
||||
- {
|
||||
profile: release,
|
||||
target: x86_64-unknown-linux-musl,
|
||||
glibc: "default",
|
||||
}
|
||||
- {
|
||||
profile: release,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
glibc: "default",
|
||||
}
|
||||
- { profile: release, target: aarch64-apple-darwin, glibc: "default" }
|
||||
#- { profile: release, target: aarch64-unknown-linux-gnu, glibc: "default" }
|
||||
- {
|
||||
profile: release,
|
||||
target: aarch64-unknown-linux-musl,
|
||||
glibc: "default",
|
||||
}
|
||||
#- { profile: release, target: x86_64-pc-windows-msvc, glibc: "default" }
|
||||
exclude:
|
||||
# Linux targets on non-Linux systems
|
||||
- os: macos-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
glibc: "default",
|
||||
}
|
||||
- os: macos-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: x86_64-unknown-linux-musl,
|
||||
glibc: "default",
|
||||
}
|
||||
- os: macos-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
glibc: "default",
|
||||
}
|
||||
- os: macos-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: aarch64-unknown-linux-musl,
|
||||
glibc: "default",
|
||||
}
|
||||
- os: windows-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
glibc: "default",
|
||||
}
|
||||
- os: windows-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: x86_64-unknown-linux-musl,
|
||||
glibc: "default",
|
||||
}
|
||||
- os: windows-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
glibc: "default",
|
||||
}
|
||||
- os: windows-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: aarch64-unknown-linux-musl,
|
||||
glibc: "default",
|
||||
}
|
||||
|
||||
# Apple targets on non-macOS systems
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: aarch64-apple-darwin,
|
||||
glibc: "default",
|
||||
}
|
||||
- os: windows-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: aarch64-apple-darwin,
|
||||
glibc: "default",
|
||||
}
|
||||
|
||||
# Windows targets on non-Windows systems
|
||||
target: x86_64-unknown-linux-musl
|
||||
cross: false
|
||||
- os: ubuntu-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: x86_64-pc-windows-msvc,
|
||||
glibc: "default",
|
||||
}
|
||||
target: aarch64-unknown-linux-musl
|
||||
cross: true
|
||||
- os: macos-latest
|
||||
variant:
|
||||
{
|
||||
profile: release,
|
||||
target: x86_64-pc-windows-msvc,
|
||||
glibc: "default",
|
||||
}
|
||||
|
||||
target: aarch64-apple-darwin
|
||||
cross: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Installation system dependencies
|
||||
- name: Install system dependencies (Ubuntu)
|
||||
if: runner.os == 'Linux'
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
rust-version: stable
|
||||
target: ${{ matrix.target }}
|
||||
cache-shared-key: build-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
|
||||
install-cross-tools: ${{ matrix.cross }}
|
||||
|
||||
- name: Download static console assets
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y musl-tools build-essential lld libdbus-1-dev libwayland-dev libwebkit2gtk-4.1-dev libxdo-dev
|
||||
shell: bash
|
||||
|
||||
#Install Rust using dtolnay/rust-toolchain
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.variant.target }}
|
||||
components: rustfmt, clippy
|
||||
|
||||
# Install system dependencies
|
||||
- name: Cache Protoc
|
||||
id: cache-protoc
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: /Users/runner/hostedtoolcache/protoc
|
||||
key: protoc-${{ runner.os }}-31.1
|
||||
restore-keys: |
|
||||
protoc-${{ runner.os }}-
|
||||
|
||||
- name: Install Protoc
|
||||
if: steps.cache-protoc.outputs.cache-hit != 'true'
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: "31.1"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Flatc
|
||||
uses: Nugine/setup-flatc@v1
|
||||
with:
|
||||
version: "25.2.10"
|
||||
|
||||
# Cache Cargo dependencies
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
cache-all-crates: true
|
||||
shared-key: rustfs-${{ matrix.os }}-${{ matrix.variant.profile }}-${{ matrix.variant.target }}-${{ matrix.variant.glibc }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
save-if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
# Set up Zig for cross-compilation
|
||||
- uses: mlugg/setup-zig@v2
|
||||
if: matrix.variant.glibc != 'default' || contains(matrix.variant.target, 'aarch64-unknown-linux')
|
||||
|
||||
- uses: taiki-e/install-action@cargo-zigbuild
|
||||
if: matrix.variant.glibc != 'default' || contains(matrix.variant.target, 'aarch64-unknown-linux')
|
||||
|
||||
# Download static resources
|
||||
- name: Download and Extract Static Assets
|
||||
run: |
|
||||
url="https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip"
|
||||
|
||||
# Create a static resource directory
|
||||
mkdir -p ./rustfs/static
|
||||
curl -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" \
|
||||
-o console.zip --retry 3 --retry-delay 5 --max-time 300
|
||||
unzip -o console.zip -d ./rustfs/static
|
||||
rm console.zip
|
||||
|
||||
# Download static resources
|
||||
echo "::group::Downloading static assets"
|
||||
curl -L -o static_assets.zip "$url" --retry 3
|
||||
|
||||
# Unzip static resources
|
||||
echo "::group::Extracting static assets"
|
||||
if [ "${{ runner.os }}" = "Windows" ]; then
|
||||
7z x static_assets.zip -o./rustfs/static
|
||||
del static_assets.zip
|
||||
else
|
||||
unzip -o static_assets.zip -d ./rustfs/static
|
||||
rm static_assets.zip
|
||||
fi
|
||||
|
||||
echo "::group::Static assets content"
|
||||
ls -la ./rustfs/static
|
||||
shell: bash
|
||||
|
||||
# Build rustfs
|
||||
- name: Build rustfs
|
||||
id: build
|
||||
shell: bash
|
||||
- name: Build RustFS
|
||||
run: |
|
||||
echo "::group::Setting up build parameters"
|
||||
PROFILE="${{ matrix.variant.profile }}"
|
||||
TARGET="${{ matrix.variant.target }}"
|
||||
GLIBC="${{ matrix.variant.glibc }}"
|
||||
|
||||
# Determine whether to use zigbuild
|
||||
USE_ZIGBUILD=false
|
||||
if [[ "$GLIBC" != "default" || "$TARGET" == *"aarch64-unknown-linux"* ]]; then
|
||||
USE_ZIGBUILD=true
|
||||
echo "Using zigbuild for cross-compilation"
|
||||
fi
|
||||
|
||||
# Determine the target parameters
|
||||
TARGET_ARG="$TARGET"
|
||||
if [[ "$GLIBC" != "default" ]]; then
|
||||
TARGET_ARG="${TARGET}.${GLIBC}"
|
||||
echo "Using custom glibc target: $TARGET_ARG"
|
||||
fi
|
||||
|
||||
# Confirm the profile directory name
|
||||
if [[ "$PROFILE" == "dev" ]]; then
|
||||
PROFILE_DIR="debug"
|
||||
else
|
||||
PROFILE_DIR="$PROFILE"
|
||||
fi
|
||||
|
||||
# Determine the binary suffix
|
||||
BIN_SUFFIX=""
|
||||
if [[ "${{ matrix.variant.target }}" == *"windows"* ]]; then
|
||||
BIN_SUFFIX=".exe"
|
||||
fi
|
||||
|
||||
# Determine the binary name - Use the appropriate extension for Windows
|
||||
BIN_NAME="rustfs.${PROFILE}.${TARGET}"
|
||||
if [[ "$GLIBC" != "default" ]]; then
|
||||
BIN_NAME="${BIN_NAME}.glibc${GLIBC}"
|
||||
fi
|
||||
|
||||
# Windows systems use exe suffix, and other systems do not have suffix
|
||||
if [[ "${{ matrix.variant.target }}" == *"windows"* ]]; then
|
||||
BIN_NAME="${BIN_NAME}.exe"
|
||||
else
|
||||
BIN_NAME="${BIN_NAME}.bin"
|
||||
fi
|
||||
|
||||
echo "Binary name will be: $BIN_NAME"
|
||||
|
||||
echo "::group::Building rustfs"
|
||||
# Refresh build information
|
||||
touch rustfs/build.rs
|
||||
|
||||
# Identify the build command and execute it
|
||||
if [[ "$USE_ZIGBUILD" == "true" ]]; then
|
||||
echo "Build command: cargo zigbuild --profile $PROFILE --target $TARGET_ARG -p rustfs --bins"
|
||||
cargo zigbuild --profile $PROFILE --target $TARGET_ARG -p rustfs --bins
|
||||
if [[ "${{ matrix.cross }}" == "true" ]]; then
|
||||
cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bins
|
||||
else
|
||||
echo "Build command: cargo build --profile $PROFILE --target $TARGET_ARG -p rustfs --bins"
|
||||
cargo build --profile $PROFILE --target $TARGET_ARG -p rustfs --bins
|
||||
cargo build --release --target ${{ matrix.target }} -p rustfs --bins
|
||||
fi
|
||||
|
||||
# Determine the binary path and output path
|
||||
BIN_PATH="target/${TARGET_ARG}/${PROFILE_DIR}/rustfs${BIN_SUFFIX}"
|
||||
OUT_PATH="target/artifacts/${BIN_NAME}"
|
||||
|
||||
# Create a target directory
|
||||
mkdir -p target/artifacts
|
||||
|
||||
echo "Copying binary from ${BIN_PATH} to ${OUT_PATH}"
|
||||
cp "${BIN_PATH}" "${OUT_PATH}"
|
||||
|
||||
# Record the output path for use in the next steps
|
||||
echo "bin_path=${OUT_PATH}" >> $GITHUB_OUTPUT
|
||||
echo "bin_name=${BIN_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Package Binary and Static Assets
|
||||
- name: Create release package
|
||||
id: package
|
||||
run: |
|
||||
# Create component file name
|
||||
ARTIFACT_NAME="rustfs-${{ matrix.variant.profile }}-${{ matrix.variant.target }}"
|
||||
if [ "${{ matrix.variant.glibc }}" != "default" ]; then
|
||||
ARTIFACT_NAME="${ARTIFACT_NAME}-glibc${{ matrix.variant.glibc }}"
|
||||
fi
|
||||
echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT
|
||||
PACKAGE_NAME="rustfs-${{ matrix.target }}"
|
||||
mkdir -p "${PACKAGE_NAME}"/{bin,docs}
|
||||
|
||||
# Get the binary path
|
||||
BIN_PATH="${{ steps.build.outputs.bin_path }}"
|
||||
|
||||
# Create a packaged directory structure - only contains bin and docs directories
|
||||
mkdir -p ${ARTIFACT_NAME}/{bin,docs}
|
||||
|
||||
# Copy binary files (note the difference between Windows and other systems)
|
||||
if [[ "${{ matrix.variant.target }}" == *"windows"* ]]; then
|
||||
cp "${BIN_PATH}" ${ARTIFACT_NAME}/bin/rustfs.exe
|
||||
# Copy binary
|
||||
if [[ "${{ matrix.target }}" == *"windows"* ]]; then
|
||||
cp target/${{ matrix.target }}/release/rustfs.exe "${PACKAGE_NAME}/bin/"
|
||||
else
|
||||
cp "${BIN_PATH}" ${ARTIFACT_NAME}/bin/rustfs
|
||||
cp target/${{ matrix.target }}/release/rustfs "${PACKAGE_NAME}/bin/"
|
||||
chmod +x "${PACKAGE_NAME}/bin/rustfs"
|
||||
fi
|
||||
|
||||
# copy documents and licenses
|
||||
if [ -f "LICENSE" ]; then
|
||||
cp LICENSE ${ARTIFACT_NAME}/docs/
|
||||
fi
|
||||
if [ -f "README.md" ]; then
|
||||
cp README.md ${ARTIFACT_NAME}/docs/
|
||||
fi
|
||||
# Copy documentation
|
||||
[ -f "LICENSE" ] && cp LICENSE "${PACKAGE_NAME}/docs/"
|
||||
[ -f "README.md" ] && cp README.md "${PACKAGE_NAME}/docs/"
|
||||
|
||||
# Packaged as zip
|
||||
if [ "${{ runner.os }}" = "Windows" ]; then
|
||||
7z a ${ARTIFACT_NAME}.zip ${ARTIFACT_NAME}
|
||||
else
|
||||
zip -r ${ARTIFACT_NAME}.zip ${ARTIFACT_NAME}
|
||||
fi
|
||||
# Create archive
|
||||
tar -czf "${PACKAGE_NAME}.tar.gz" "${PACKAGE_NAME}"
|
||||
|
||||
echo "Created artifact: ${ARTIFACT_NAME}.zip"
|
||||
ls -la ${ARTIFACT_NAME}.zip
|
||||
shell: bash
|
||||
echo "package_name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "Package created: ${PACKAGE_NAME}.tar.gz"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.package.outputs.artifact_name }}
|
||||
path: ${{ steps.package.outputs.artifact_name }}.zip
|
||||
retention-days: 7
|
||||
name: ${{ steps.package.outputs.package_name }}
|
||||
path: ${{ steps.package.outputs.package_name }}.tar.gz
|
||||
retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 30 || 7 }}
|
||||
|
||||
# Install ossutil2 tool for OSS upload
|
||||
- name: Install ossutil2
|
||||
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main'
|
||||
shell: bash
|
||||
# Build GUI (only for releases)
|
||||
build-gui:
|
||||
name: Build GUI
|
||||
needs: [skip-duplicate, build-check, build-rustfs]
|
||||
if: needs.skip-duplicate.outputs.should_skip != 'true' && needs.build-check.outputs.build_type == 'release'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
platform: linux
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
platform: macos
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
rust-version: stable
|
||||
target: ${{ matrix.target }}
|
||||
cache-shared-key: gui-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download RustFS binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rustfs-${{ matrix.target }}
|
||||
path: ./artifacts
|
||||
|
||||
- name: Prepare embedded binary
|
||||
run: |
|
||||
echo "::group::Installing ossutil2"
|
||||
# Download and install ossutil based on platform
|
||||
if [ "${{ runner.os }}" = "Linux" ]; then
|
||||
curl -o ossutil.zip https://gosspublic.alicdn.com/ossutil/v2/2.1.1/ossutil-2.1.1-linux-amd64.zip
|
||||
unzip -o ossutil.zip
|
||||
chmod 755 ossutil-2.1.1-linux-amd64/ossutil
|
||||
sudo mv ossutil-2.1.1-linux-amd64/ossutil /usr/local/bin/
|
||||
rm -rf ossutil.zip ossutil-2.1.1-linux-amd64
|
||||
elif [ "${{ runner.os }}" = "macOS" ]; then
|
||||
if [ "$(uname -m)" = "arm64" ]; then
|
||||
curl -o ossutil.zip https://gosspublic.alicdn.com/ossutil/v2/2.1.1/ossutil-2.1.1-mac-arm64.zip
|
||||
else
|
||||
curl -o ossutil.zip https://gosspublic.alicdn.com/ossutil/v2/2.1.1/ossutil-2.1.1-mac-amd64.zip
|
||||
fi
|
||||
unzip -o ossutil.zip
|
||||
chmod 755 ossutil-*/ossutil
|
||||
sudo mv ossutil-*/ossutil /usr/local/bin/
|
||||
rm -rf ossutil.zip ossutil-*
|
||||
elif [ "${{ runner.os }}" = "Windows" ]; then
|
||||
curl -o ossutil.zip https://gosspublic.alicdn.com/ossutil/v2/2.1.1/ossutil-2.1.1-windows-amd64.zip
|
||||
unzip -o ossutil.zip
|
||||
mv ossutil-*/ossutil.exe /usr/bin/ossutil.exe
|
||||
rm -rf ossutil.zip ossutil-*
|
||||
fi
|
||||
echo "ossutil2 installation completed"
|
||||
|
||||
- name: Upload to Aliyun OSS
|
||||
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main'
|
||||
shell: bash
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.ALICLOUDOSS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUDOSS_KEY_SECRET }}
|
||||
OSS_REGION: cn-beijing
|
||||
OSS_ENDPOINT: https://oss-cn-beijing.aliyuncs.com
|
||||
run: |
|
||||
echo "::group::Uploading files to OSS"
|
||||
# Upload the artifact file to two different paths
|
||||
ossutil cp "${{ steps.package.outputs.artifact_name }}.zip" "oss://rustfs-artifacts/artifacts/rustfs/${{ steps.package.outputs.artifact_name }}.zip" --force
|
||||
ossutil cp "${{ steps.package.outputs.artifact_name }}.zip" "oss://rustfs-artifacts/artifacts/rustfs/${{ steps.package.outputs.artifact_name }}.latest.zip" --force
|
||||
echo "Successfully uploaded artifacts to OSS"
|
||||
|
||||
# Create and upload latest version info
|
||||
- name: Create and Upload latest.json
|
||||
if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-latest' && matrix.variant.target == 'x86_64-unknown-linux-musl'
|
||||
shell: bash
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.ALICLOUDOSS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUDOSS_KEY_SECRET }}
|
||||
OSS_REGION: cn-beijing
|
||||
OSS_ENDPOINT: https://oss-cn-beijing.aliyuncs.com
|
||||
run: |
|
||||
echo "::group::Creating latest.json file"
|
||||
|
||||
# Extract version from tag (remove 'refs/tags/' prefix)
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
# Remove 'v' prefix if present
|
||||
VERSION="${VERSION#v}"
|
||||
|
||||
# Get current timestamp in ISO 8601 format
|
||||
RELEASE_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Create latest.json content
|
||||
cat > latest.json << EOF
|
||||
{
|
||||
"version": "${VERSION}",
|
||||
"release_date": "${RELEASE_DATE}",
|
||||
"release_notes": "Release ${VERSION}",
|
||||
"download_url": "https://github.com/rustfs/rustfs/releases/tag/${GITHUB_REF#refs/tags/}"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Generated latest.json:"
|
||||
cat latest.json
|
||||
|
||||
echo "::group::Uploading latest.json to OSS"
|
||||
# Upload latest.json to rustfs-version bucket
|
||||
ossutil cp latest.json "oss://rustfs-version/latest.json" --force
|
||||
echo "Successfully uploaded latest.json to OSS"
|
||||
|
||||
# Determine whether to perform GUI construction based on conditions
|
||||
- name: Prepare for GUI build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
id: prepare_gui
|
||||
run: |
|
||||
# Create a target directory
|
||||
mkdir -p ./cli/rustfs-gui/embedded-rustfs/
|
||||
tar -xzf ./artifacts/rustfs-${{ matrix.target }}.tar.gz -C ./artifacts/
|
||||
cp ./artifacts/rustfs-${{ matrix.target }}/bin/rustfs ./cli/rustfs-gui/embedded-rustfs/
|
||||
|
||||
# Copy the currently built binary to the embedded-rustfs directory
|
||||
if [[ "${{ matrix.variant.target }}" == *"windows"* ]]; then
|
||||
cp "${{ steps.build.outputs.bin_path }}" ./cli/rustfs-gui/embedded-rustfs/rustfs.exe
|
||||
else
|
||||
cp "${{ steps.build.outputs.bin_path }}" ./cli/rustfs-gui/embedded-rustfs/rustfs
|
||||
fi
|
||||
|
||||
echo "Copied binary to embedded-rustfs directory"
|
||||
ls -la ./cli/rustfs-gui/embedded-rustfs/
|
||||
shell: bash
|
||||
|
||||
#Install the dioxus-cli tool
|
||||
- uses: taiki-e/cache-cargo-install-action@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
- name: Install Dioxus CLI
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: dioxus-cli
|
||||
|
||||
# Build and package GUI applications
|
||||
- name: Build and Bundle rustfs-gui
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
id: build_gui
|
||||
shell: bash
|
||||
- name: Build GUI
|
||||
working-directory: ./cli/rustfs-gui
|
||||
run: |
|
||||
echo "::group::Setting up build parameters for GUI"
|
||||
PROFILE="${{ matrix.variant.profile }}"
|
||||
TARGET="${{ matrix.variant.target }}"
|
||||
GLIBC="${{ matrix.variant.glibc }}"
|
||||
RELEASE_PATH="target/artifacts/$TARGET"
|
||||
|
||||
# Make sure the output directory exists
|
||||
mkdir -p ${RELEASE_PATH}
|
||||
|
||||
# Configure the target platform linker
|
||||
echo "::group::Configuring linker for $TARGET"
|
||||
case "$TARGET" in
|
||||
"x86_64-unknown-linux-gnu")
|
||||
export CC_x86_64_unknown_linux_gnu=gcc
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=gcc
|
||||
;;
|
||||
"x86_64-unknown-linux-musl")
|
||||
export CC_x86_64_unknown_linux_musl=musl-gcc
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc
|
||||
;;
|
||||
"aarch64-unknown-linux-gnu")
|
||||
export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||
;;
|
||||
"aarch64-unknown-linux-musl")
|
||||
export CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc
|
||||
;;
|
||||
"aarch64-apple-darwin")
|
||||
export CC_aarch64_apple_darwin=clang
|
||||
export CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=clang
|
||||
;;
|
||||
"x86_64-pc-windows-msvc")
|
||||
export CC_x86_64_pc_windows_msvc=cl
|
||||
export CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER=link
|
||||
;;
|
||||
case "${{ matrix.platform }}" in
|
||||
"linux")
|
||||
dx bundle --platform linux --package-types deb --package-types appimage --release
|
||||
;;
|
||||
"macos")
|
||||
dx bundle --platform macos --package-types dmg --release
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "::group::Building GUI application"
|
||||
cd cli/rustfs-gui
|
||||
|
||||
# Building according to the target platform
|
||||
if [[ "$TARGET" == *"apple-darwin"* ]]; then
|
||||
echo "Building for macOS"
|
||||
dx bundle --platform macos --package-types "macos" --package-types "dmg" --release --profile ${PROFILE} --out-dir ../../${RELEASE_PATH}
|
||||
elif [[ "$TARGET" == *"windows-msvc"* ]]; then
|
||||
echo "Building for Windows"
|
||||
dx bundle --platform windows --package-types "msi" --release --profile ${PROFILE} --out-dir ../../${RELEASE_PATH}
|
||||
elif [[ "$TARGET" == *"linux"* ]]; then
|
||||
echo "Building for Linux"
|
||||
dx bundle --platform linux --package-types "deb" --package-types "rpm" --package-types "appimage" --release --profile ${PROFILE} --out-dir ../../${RELEASE_PATH}
|
||||
fi
|
||||
|
||||
cd ../..
|
||||
|
||||
# Create component name
|
||||
GUI_ARTIFACT_NAME="rustfs-gui-${PROFILE}-${TARGET}"
|
||||
|
||||
if [ "$GLIBC" != "default" ]; then
|
||||
GUI_ARTIFACT_NAME="${GUI_ARTIFACT_NAME}-glibc${GLIBC}"
|
||||
fi
|
||||
|
||||
echo "::group::Packaging GUI application"
|
||||
# Select packaging method according to the operating system
|
||||
if [ "${{ runner.os }}" = "Windows" ]; then
|
||||
7z a ${GUI_ARTIFACT_NAME}.zip ${RELEASE_PATH}/*
|
||||
else
|
||||
zip -r ${GUI_ARTIFACT_NAME}.zip ${RELEASE_PATH}/*
|
||||
fi
|
||||
|
||||
echo "gui_artifact_name=${GUI_ARTIFACT_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "Created GUI artifact: ${GUI_ARTIFACT_NAME}.zip"
|
||||
ls -la ${GUI_ARTIFACT_NAME}.zip
|
||||
|
||||
# Upload GUI components
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: ${{ steps.build_gui.outputs.gui_artifact_name }}
|
||||
path: ${{ steps.build_gui.outputs.gui_artifact_name }}.zip
|
||||
retention-days: 7
|
||||
|
||||
# Upload GUI to Alibaba Cloud OSS
|
||||
- name: Upload GUI to Aliyun OSS
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: bash
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.ALICLOUDOSS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUDOSS_KEY_SECRET }}
|
||||
OSS_REGION: cn-beijing
|
||||
OSS_ENDPOINT: https://oss-cn-beijing.aliyuncs.com
|
||||
- name: Package GUI
|
||||
id: gui_package
|
||||
run: |
|
||||
echo "::group::Uploading GUI files to OSS"
|
||||
# Upload the GUI artifact file to two different paths
|
||||
ossutil cp "${{ steps.build_gui.outputs.gui_artifact_name }}.zip" "oss://rustfs-artifacts/artifacts/rustfs/${{ steps.build_gui.outputs.gui_artifact_name }}.zip" --force
|
||||
ossutil cp "${{ steps.build_gui.outputs.gui_artifact_name }}.zip" "oss://rustfs-artifacts/artifacts/rustfs/${{ steps.build_gui.outputs.gui_artifact_name }}.latest.zip" --force
|
||||
echo "Successfully uploaded GUI artifacts to OSS"
|
||||
GUI_PACKAGE="rustfs-gui-${{ matrix.target }}"
|
||||
mkdir -p "${GUI_PACKAGE}"
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-rustfs]
|
||||
# Only execute merge operation when tag is pushed
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- uses: actions/upload-artifact/merge@v4
|
||||
# Copy GUI bundles
|
||||
if [[ -d "cli/rustfs-gui/dist/bundle" ]]; then
|
||||
cp -r cli/rustfs-gui/dist/bundle/* "${GUI_PACKAGE}/"
|
||||
fi
|
||||
|
||||
tar -czf "${GUI_PACKAGE}.tar.gz" "${GUI_PACKAGE}"
|
||||
echo "gui_package=${GUI_PACKAGE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload GUI artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rustfs-packages
|
||||
pattern: "rustfs-*"
|
||||
delete-merged: true
|
||||
name: ${{ steps.gui_package.outputs.gui_package }}
|
||||
path: ${{ steps.gui_package.outputs.gui_package }}.tar.gz
|
||||
retention-days: 30
|
||||
|
||||
# Release management
|
||||
release:
|
||||
name: GitHub Release
|
||||
needs: [skip-duplicate, build-check, build-rustfs, build-gui]
|
||||
if: always() && needs.skip-duplicate.outputs.should_skip != 'true' && needs.build-check.outputs.build_type == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./release-artifacts
|
||||
|
||||
- name: Prepare release
|
||||
id: release_prep
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
VERSION_CLEAN="${VERSION#v}"
|
||||
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "version_clean=${VERSION_CLEAN}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Organize artifacts
|
||||
mkdir -p ./release-files
|
||||
find ./release-artifacts -name "*.tar.gz" -exec cp {} ./release-files/ \;
|
||||
|
||||
# Create release notes
|
||||
cat > release_notes.md << EOF
|
||||
## RustFS ${VERSION_CLEAN}
|
||||
|
||||
### 🚀 Downloads
|
||||
|
||||
**Linux:**
|
||||
- \`rustfs-x86_64-unknown-linux-musl.tar.gz\` - Linux x86_64 (static)
|
||||
- \`rustfs-aarch64-unknown-linux-musl.tar.gz\` - Linux ARM64 (static)
|
||||
|
||||
**macOS:**
|
||||
- \`rustfs-aarch64-apple-darwin.tar.gz\` - macOS Apple Silicon
|
||||
|
||||
**GUI Applications:**
|
||||
- \`rustfs-gui-*.tar.gz\` - GUI applications
|
||||
|
||||
### 📦 Installation
|
||||
|
||||
1. Download the appropriate binary for your platform
|
||||
2. Extract: \`tar -xzf rustfs-*.tar.gz\`
|
||||
3. Install: \`sudo cp rustfs-*/bin/rustfs /usr/local/bin/\`
|
||||
|
||||
### 🔗 Mirror Downloads
|
||||
|
||||
- [OSS Mirror](https://rustfs-artifacts.oss-cn-beijing.aliyuncs.com/artifacts/rustfs/)
|
||||
EOF
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.release_prep.outputs.version }}
|
||||
name: "RustFS ${{ steps.release_prep.outputs.version_clean }}"
|
||||
body_path: release_notes.md
|
||||
files: ./release-files/*.tar.gz
|
||||
draft: false
|
||||
prerelease: ${{ contains(steps.release_prep.outputs.version, 'alpha') || contains(steps.release_prep.outputs.version, 'beta') || contains(steps.release_prep.outputs.version, 'rc') }}
|
||||
|
||||
# Upload to OSS (optional)
|
||||
upload-oss:
|
||||
name: Upload to OSS
|
||||
needs: [skip-duplicate, build-check, build-rustfs]
|
||||
if: always() && needs.skip-duplicate.outputs.should_skip != 'true' && needs.build-check.outputs.build_type == 'release' && needs.build-rustfs.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.ALICLOUDOSS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUDOSS_KEY_SECRET }}
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Upload to Aliyun OSS
|
||||
if: ${{ env.OSS_ACCESS_KEY_ID != '' }}
|
||||
run: |
|
||||
# Install ossutil
|
||||
curl -o ossutil.zip https://gosspublic.alicdn.com/ossutil/v2/2.1.1/ossutil-2.1.1-linux-amd64.zip
|
||||
unzip ossutil.zip
|
||||
sudo mv ossutil-*/ossutil /usr/local/bin/
|
||||
|
||||
# Upload files
|
||||
find ./artifacts -name "*.tar.gz" -exec ossutil cp {} oss://rustfs-artifacts/artifacts/rustfs/ --force \;
|
||||
|
||||
# Create latest.json
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
echo "{\"version\":\"${VERSION}\",\"release_date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > latest.json
|
||||
ossutil cp latest.json oss://rustfs-version/latest.json --force
|
||||
|
||||
+49
-25
@@ -12,12 +12,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: CI
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "**.txt"
|
||||
@@ -35,10 +34,9 @@ on:
|
||||
- ".github/workflows/build.yml"
|
||||
- ".github/workflows/docker.yml"
|
||||
- ".github/workflows/audit.yml"
|
||||
- ".github/workflows/samply.yml"
|
||||
- ".github/workflows/performance.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "**.txt"
|
||||
@@ -56,13 +54,18 @@ on:
|
||||
- ".github/workflows/build.yml"
|
||||
- ".github/workflows/docker.yml"
|
||||
- ".github/workflows/audit.yml"
|
||||
- ".github/workflows/samply.yml"
|
||||
- ".github/workflows/performance.yml"
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # at midnight of each sunday
|
||||
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
skip-check:
|
||||
name: Skip Duplicate Actions
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
@@ -70,59 +73,80 @@ jobs:
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
- name: Skip duplicate actions
|
||||
id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
cancel_others: true
|
||||
paths_ignore: '["*.md"]'
|
||||
paths_ignore: '["*.md", "docs/**", "deploy/**"]'
|
||||
|
||||
develop:
|
||||
test-and-lint:
|
||||
name: Test and Lint
|
||||
needs: skip-check
|
||||
if: needs.skip-check.outputs.should_skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
rust-version: stable
|
||||
cache-shared-key: ci-test-${{ hashFiles('**/Cargo.lock') }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --all --exclude e2e_test
|
||||
|
||||
- name: Format
|
||||
- name: Check code formatting
|
||||
run: cargo fmt --all --check
|
||||
|
||||
- name: Lint
|
||||
- name: Run clippy lints
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
s3s-e2e:
|
||||
name: E2E (s3s-e2e)
|
||||
e2e-tests:
|
||||
name: End-to-End Tests
|
||||
needs: skip-check
|
||||
if: needs.skip-check.outputs.should_skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: ./.github/actions/setup
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install s3s-e2e
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
rust-version: stable
|
||||
cache-shared-key: ci-e2e-${{ hashFiles('**/Cargo.lock') }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install s3s-e2e test tool
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: s3s-e2e
|
||||
git: https://github.com/Nugine/s3s.git
|
||||
rev: b7714bfaa17ddfa9b23ea01774a1e7bbdbfc2ca3
|
||||
|
||||
- name: Build debug
|
||||
- name: Build debug binary
|
||||
run: |
|
||||
touch rustfs/build.rs
|
||||
cargo build -p rustfs --bins
|
||||
|
||||
- name: Run s3s-e2e
|
||||
- name: Run end-to-end tests
|
||||
run: |
|
||||
s3s-e2e --version
|
||||
./scripts/e2e-run.sh ./target/debug/rustfs /tmp/rustfs
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- name: Upload test logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: s3s-e2e.logs
|
||||
name: e2e-test-logs-${{ github.run_number }}
|
||||
path: /tmp/rustfs.log
|
||||
retention-days: 3
|
||||
|
||||
+119
-163
@@ -12,155 +12,100 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: Build and Push Docker Images
|
||||
name: Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
branches:
|
||||
- main
|
||||
tags: ["v*"]
|
||||
branches: [main]
|
||||
paths:
|
||||
- "rustfs/**"
|
||||
- "crates/**"
|
||||
- "Dockerfile*"
|
||||
- ".docker/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".github/workflows/docker.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
paths:
|
||||
- "rustfs/**"
|
||||
- "crates/**"
|
||||
- "Dockerfile*"
|
||||
- ".docker/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".github/workflows/docker.yml"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push_to_registry:
|
||||
description: "Push images to registry"
|
||||
push_images:
|
||||
description: "Push images to registries"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
REGISTRY_IMAGE_DOCKERHUB: rustfs/rustfs
|
||||
REGISTRY_IMAGE_GHCR: ghcr.io/${{ github.repository }}
|
||||
CARGO_TERM_COLOR: always
|
||||
REGISTRY_DOCKERHUB: rustfs/rustfs
|
||||
REGISTRY_GHCR: ghcr.io/${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
# Skip duplicate job runs
|
||||
skip-check:
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
# Check if we should build
|
||||
build-check:
|
||||
name: Build Check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
should_build: ${{ steps.check.outputs.should_build }}
|
||||
should_push: ${{ steps.check.outputs.should_push }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
cancel_others: true
|
||||
paths_ignore: '["*.md", "docs/**"]'
|
||||
|
||||
# Build RustFS binary for different platforms
|
||||
build-binary:
|
||||
needs: skip-check
|
||||
# Only execute in the following cases: 1) tag push 2) commit message contains --build 3) workflow_dispatch 4) PR
|
||||
if: needs.skip-check.outputs.should_skip != 'true' && (startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '--build') || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
arch: amd64
|
||||
use_cross: false
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
arch: arm64
|
||||
use_cross: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install cross-compilation dependencies (native build)
|
||||
if: matrix.use_cross == false
|
||||
- name: Check build conditions
|
||||
id: check
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools
|
||||
should_build=false
|
||||
should_push=false
|
||||
|
||||
- name: Install cross tool (cross compilation)
|
||||
if: matrix.use_cross == true
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cross
|
||||
# Always build on workflow_dispatch or when changes detected
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \
|
||||
[[ "${{ github.event_name }}" == "push" ]] || \
|
||||
[[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
should_build=true
|
||||
fi
|
||||
|
||||
- name: Install protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: "31.1"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Push only on main branch, tags, or manual trigger
|
||||
if [[ "${{ github.ref }}" == "refs/heads/main" ]] || \
|
||||
[[ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]] || \
|
||||
[[ "${{ github.event.inputs.push_images }}" == "true" ]]; then
|
||||
should_push=true
|
||||
fi
|
||||
|
||||
- name: Install flatc
|
||||
uses: Nugine/setup-flatc@v1
|
||||
with:
|
||||
version: "25.2.10"
|
||||
echo "should_build=$should_build" >> $GITHUB_OUTPUT
|
||||
echo "should_push=$should_push" >> $GITHUB_OUTPUT
|
||||
echo "Build: $should_build, Push: $should_push"
|
||||
|
||||
- name: Cache cargo dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-${{ matrix.target }}-
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Generate protobuf code
|
||||
run: cargo run --bin gproto
|
||||
|
||||
- name: Build RustFS binary (native)
|
||||
if: matrix.use_cross == false
|
||||
run: |
|
||||
cargo build --release --target ${{ matrix.target }} --bin rustfs
|
||||
|
||||
- name: Build RustFS binary (cross)
|
||||
if: matrix.use_cross == true
|
||||
run: |
|
||||
cross build --release --target ${{ matrix.target }} --bin rustfs
|
||||
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rustfs-${{ matrix.arch }}
|
||||
path: target/${{ matrix.target }}/release/rustfs
|
||||
retention-days: 1
|
||||
|
||||
# Build and push multi-arch Docker images
|
||||
build-images:
|
||||
needs: [skip-check, build-binary]
|
||||
if: needs.skip-check.outputs.should_skip != 'true'
|
||||
# Build multi-arch Docker images
|
||||
build-docker:
|
||||
name: Build Docker Images
|
||||
needs: build-check
|
||||
if: needs.build-check.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image-type: [production, ubuntu, rockylinux, devenv]
|
||||
variant:
|
||||
- name: production
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: ubuntu
|
||||
dockerfile: .docker/Dockerfile.ubuntu22.04
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: alpine
|
||||
dockerfile: .docker/Dockerfile.alpine
|
||||
platforms: linux/amd64,linux/arm64
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download binary artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Setup binary files
|
||||
run: |
|
||||
mkdir -p target/x86_64-unknown-linux-musl/release
|
||||
mkdir -p target/aarch64-unknown-linux-gnu/release
|
||||
cp artifacts/rustfs-amd64/rustfs target/x86_64-unknown-linux-musl/release/
|
||||
cp artifacts/rustfs-arm64/rustfs target/aarch64-unknown-linux-gnu/release/
|
||||
chmod +x target/*/release/rustfs
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -168,75 +113,86 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: needs.build-check.outputs.should_push == 'true' && secrets.DOCKERHUB_USERNAME != ''
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: needs.build-check.outputs.should_push == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set Dockerfile and context
|
||||
id: dockerfile
|
||||
run: |
|
||||
case "${{ matrix.image-type }}" in
|
||||
production)
|
||||
echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT
|
||||
echo "context=." >> $GITHUB_OUTPUT
|
||||
echo "suffix=" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
ubuntu)
|
||||
echo "dockerfile=.docker/Dockerfile.ubuntu22.04" >> $GITHUB_OUTPUT
|
||||
echo "context=." >> $GITHUB_OUTPUT
|
||||
echo "suffix=-ubuntu22.04" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
rockylinux)
|
||||
echo "dockerfile=.docker/Dockerfile.rockylinux9.3" >> $GITHUB_OUTPUT
|
||||
echo "context=." >> $GITHUB_OUTPUT
|
||||
echo "suffix=-rockylinux9.3" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
devenv)
|
||||
echo "dockerfile=.docker/Dockerfile.devenv" >> $GITHUB_OUTPUT
|
||||
echo "context=." >> $GITHUB_OUTPUT
|
||||
echo "suffix=-devenv" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY_IMAGE_DOCKERHUB }}
|
||||
${{ env.REGISTRY_IMAGE_GHCR }}
|
||||
${{ env.REGISTRY_DOCKERHUB }}
|
||||
${{ env.REGISTRY_GHCR }}
|
||||
tags: |
|
||||
type=ref,event=branch,suffix=${{ steps.dockerfile.outputs.suffix }}
|
||||
type=ref,event=pr,suffix=${{ steps.dockerfile.outputs.suffix }}
|
||||
type=semver,pattern={{version}},suffix=${{ steps.dockerfile.outputs.suffix }}
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=${{ steps.dockerfile.outputs.suffix }}
|
||||
type=semver,pattern={{major}},suffix=${{ steps.dockerfile.outputs.suffix }}
|
||||
type=raw,value=latest,suffix=${{ steps.dockerfile.outputs.suffix }},enable={{is_default_branch}}
|
||||
type=ref,event=branch,suffix=-${{ matrix.variant.name }}
|
||||
type=ref,event=pr,suffix=-${{ matrix.variant.name }}
|
||||
type=semver,pattern={{version}},suffix=-${{ matrix.variant.name }}
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.variant.name }}
|
||||
type=raw,value=latest,suffix=-${{ matrix.variant.name }},enable={{is_default_branch}}
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: Build and push multi-arch Docker image
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ${{ steps.dockerfile.outputs.context }}
|
||||
file: ${{ steps.dockerfile.outputs.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ (github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))) || github.event.inputs.push_to_registry == 'true' }}
|
||||
context: .
|
||||
file: ${{ matrix.variant.dockerfile }}
|
||||
platforms: ${{ matrix.variant.platforms }}
|
||||
push: ${{ needs.build-check.outputs.should_push == 'true' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.image-type }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.image-type }}
|
||||
cache-from: type=gha,scope=docker-${{ matrix.variant.name }}
|
||||
cache-to: type=gha,mode=max,scope=docker-${{ matrix.variant.name }}
|
||||
build-args: |
|
||||
BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
|
||||
# Create manifest for main production image
|
||||
create-manifest:
|
||||
name: Create Manifest
|
||||
needs: [build-check, build-docker]
|
||||
if: needs.build-check.outputs.should_push == 'true' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
if: secrets.DOCKERHUB_USERNAME != ''
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifest
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
|
||||
# Create main image tag (without variant suffix)
|
||||
if [[ -n "${{ secrets.DOCKERHUB_USERNAME }}" ]]; then
|
||||
docker buildx imagetools create \
|
||||
-t ${{ env.REGISTRY_DOCKERHUB }}:${VERSION} \
|
||||
-t ${{ env.REGISTRY_DOCKERHUB }}:latest \
|
||||
${{ env.REGISTRY_DOCKERHUB }}:${VERSION}-production
|
||||
fi
|
||||
|
||||
docker buildx imagetools create \
|
||||
-t ${{ env.REGISTRY_GHCR }}:${VERSION} \
|
||||
-t ${{ env.REGISTRY_GHCR }}:latest \
|
||||
${{ env.REGISTRY_GHCR }}:${VERSION}-production
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: 'issue-translator'
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: false
|
||||
# not require, default false, . Decide whether to modify the issue title
|
||||
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
|
||||
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.
|
||||
# not require. Customize the translation robot prefix message.
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
name: Performance Testing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "rustfs/**"
|
||||
- "crates/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
profile_duration:
|
||||
description: "Profiling duration in seconds"
|
||||
required: false
|
||||
default: "120"
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
performance-profile:
|
||||
name: Performance Profiling
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
rust-version: nightly
|
||||
cache-shared-key: perf-${{ hashFiles('**/Cargo.lock') }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install additional nightly components
|
||||
run: rustup component add llvm-tools-preview
|
||||
|
||||
- name: Install samply profiler
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: samply
|
||||
|
||||
- name: Configure kernel for profiling
|
||||
run: echo '1' | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
|
||||
- name: Prepare test environment
|
||||
run: |
|
||||
# Create test volumes
|
||||
for i in {0..4}; do
|
||||
mkdir -p ./target/volume/test$i
|
||||
done
|
||||
|
||||
# Set environment variables
|
||||
echo "RUSTFS_VOLUMES=./target/volume/test{0...4}" >> $GITHUB_ENV
|
||||
echo "RUST_LOG=rustfs=info,ecstore=info,s3s=info,iam=info,rustfs-obs=info" >> $GITHUB_ENV
|
||||
|
||||
- name: Download static files
|
||||
run: |
|
||||
curl -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" \
|
||||
-o tempfile.zip --retry 3 --retry-delay 5
|
||||
unzip -o tempfile.zip -d ./rustfs/static
|
||||
rm tempfile.zip
|
||||
|
||||
- name: Build with profiling optimizations
|
||||
run: |
|
||||
RUSTFLAGS="-C force-frame-pointers=yes -C debug-assertions=off" \
|
||||
cargo +nightly build --profile profiling -p rustfs --bins
|
||||
|
||||
- name: Run performance profiling
|
||||
id: profiling
|
||||
run: |
|
||||
DURATION="${{ github.event.inputs.profile_duration || '120' }}"
|
||||
echo "Running profiling for ${DURATION} seconds..."
|
||||
|
||||
timeout "${DURATION}s" samply record \
|
||||
--output samply-profile.json \
|
||||
./target/profiling/rustfs ${RUSTFS_VOLUMES} || true
|
||||
|
||||
if [ -f "samply-profile.json" ]; then
|
||||
echo "profile_generated=true" >> $GITHUB_OUTPUT
|
||||
echo "Profile generated successfully"
|
||||
else
|
||||
echo "profile_generated=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Profile data not generated"
|
||||
fi
|
||||
|
||||
- name: Upload profile data
|
||||
if: steps.profiling.outputs.profile_generated == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: performance-profile-${{ github.run_number }}
|
||||
path: samply-profile.json
|
||||
retention-days: 30
|
||||
|
||||
benchmark:
|
||||
name: Benchmark Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
rust-version: stable
|
||||
cache-shared-key: bench-${{ hashFiles('**/Cargo.lock') }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Run benchmarks
|
||||
run: |
|
||||
cargo bench --package ecstore --bench comparison_benchmark -- --output-format json | \
|
||||
tee benchmark-results.json
|
||||
|
||||
- name: Upload benchmark results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark-results-${{ github.run_number }}
|
||||
path: benchmark-results.json
|
||||
retention-days: 7
|
||||
@@ -1,82 +0,0 @@
|
||||
# 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.
|
||||
|
||||
name: Profile with Samply
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: llvm-tools-preview
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install samply
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: samply
|
||||
|
||||
- name: Configure kernel for profiling
|
||||
run: echo '1' | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
|
||||
- name: Create test volumes
|
||||
run: |
|
||||
for i in {0..4}; do
|
||||
mkdir -p ./target/volume/test$i
|
||||
done
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "RUSTFS_VOLUMES=./target/volume/test{0...4}" >> $GITHUB_ENV
|
||||
echo "RUST_LOG=rustfs=info,ecstore=info,s3s=info,iam=info,rustfs-obs=info" >> $GITHUB_ENV
|
||||
|
||||
- name: Download static files
|
||||
run: |
|
||||
curl -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" -o tempfile.zip && unzip -o tempfile.zip -d ./rustfs/static && rm tempfile.zip
|
||||
|
||||
- name: Build with profiling
|
||||
run: |
|
||||
RUSTFLAGS="-C force-frame-pointers=yes" cargo +nightly build --profile profiling -p rustfs --bins
|
||||
|
||||
- name: Run samply with timeout
|
||||
id: samply_record
|
||||
run: |
|
||||
timeout 120s samply record --output samply.json ./target/profiling/rustfs ${RUSTFS_VOLUMES}
|
||||
if [ -f "samply.json" ]; then
|
||||
echo "profile_generated=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "profile_generated=false" >> $GITHUB_OUTPUT
|
||||
echo "::error::Failed to generate profile data"
|
||||
fi
|
||||
|
||||
- name: Upload profile data
|
||||
if: steps.samply_record.outputs.profile_generated == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: samply-profile-${{ github.run_number }}
|
||||
path: samply.json
|
||||
retention-days: 7
|
||||
@@ -11,21 +11,25 @@
|
||||
Before every commit, you **MUST**:
|
||||
|
||||
1. **Format your code**:
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
```
|
||||
|
||||
2. **Verify formatting**:
|
||||
|
||||
```bash
|
||||
cargo fmt --all --check
|
||||
```
|
||||
|
||||
3. **Pass clippy checks**:
|
||||
|
||||
```bash
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
```
|
||||
|
||||
4. **Ensure compilation**:
|
||||
|
||||
```bash
|
||||
cargo check --all-targets
|
||||
```
|
||||
@@ -136,6 +140,7 @@ Install the `rust-analyzer` extension and add to your `settings.json`:
|
||||
#### Other IDEs
|
||||
|
||||
Configure your IDE to:
|
||||
|
||||
- Use the project's `rustfmt.toml` configuration
|
||||
- Format on save
|
||||
- Run clippy checks
|
||||
Generated
+52
-36
@@ -472,9 +472,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
checksum = "16c74e56284d2188cabb6ad99603d1ace887a5d7e7b695d01b728155ed9ed427"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
@@ -733,9 +733,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.95.0"
|
||||
version = "1.96.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a316e3c4c38837084dfbf87c0fc6ea016b3dc3e1f867d9d7f5eddfe47e5cae37"
|
||||
checksum = "6e25d24de44b34dcdd5182ac4e4c6f07bcec2661c505acef94c0d293b65505fe"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -2058,7 +2058,6 @@ dependencies = [
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"futures",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
@@ -2071,7 +2070,6 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"tokio",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -3471,7 +3469,7 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "e2e_test"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"flatbuffers 25.2.10",
|
||||
@@ -4948,6 +4946,17 @@ version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02"
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -5625,9 +5634,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
@@ -7830,7 +7839,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"atoi",
|
||||
@@ -7899,7 +7908,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-appauth"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"rsa",
|
||||
@@ -7909,7 +7918,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-common"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"tokio",
|
||||
@@ -7918,7 +7927,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-config"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"const-str",
|
||||
"serde",
|
||||
@@ -7927,7 +7936,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-crypto"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -7945,7 +7954,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-ecstore"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-trait",
|
||||
@@ -8020,7 +8029,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-filemeta"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
@@ -8041,7 +8050,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-gui"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dioxus",
|
||||
@@ -8062,7 +8071,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-iam"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
@@ -8086,7 +8095,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-lock"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"lazy_static",
|
||||
@@ -8103,7 +8112,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-madmin"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"humantime",
|
||||
@@ -8115,7 +8124,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-notify"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -8144,7 +8153,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-obs"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -8177,7 +8186,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-policy"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"ipnetwork",
|
||||
@@ -8196,7 +8205,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-protos"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"flatbuffers 25.2.10",
|
||||
"prost",
|
||||
@@ -8207,12 +8216,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-rio"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"bytes",
|
||||
"crc32fast",
|
||||
"criterion",
|
||||
"futures",
|
||||
"http 1.3.1",
|
||||
"md-5",
|
||||
@@ -8256,7 +8264,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-s3select-api"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -8280,7 +8288,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-s3select-query"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
@@ -8298,21 +8306,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-signer"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"lazy_static",
|
||||
"rand 0.9.1",
|
||||
"rustfs-utils",
|
||||
"s3s",
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
"tempfile",
|
||||
"time",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-utils"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"blake3",
|
||||
@@ -8356,7 +8368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-workers"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -8364,7 +8376,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-zip"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"tokio",
|
||||
@@ -8552,8 +8564,9 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "s3s"
|
||||
version = "0.12.0-dev"
|
||||
source = "git+https://github.com/Nugine/s3s.git?rev=4733cdfb27b2713e832967232cbff413bb768c10#4733cdfb27b2713e832967232cbff413bb768c10"
|
||||
version = "0.12.0-minio-preview.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b630a6b9051328a0c185cacf723180ccd7936d08f1fda0b932a60b1b9cd860d"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"async-trait",
|
||||
@@ -9834,17 +9847,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.1"
|
||||
version = "1.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
@@ -10085,6 +10100,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
|
||||
+30
-30
@@ -44,7 +44,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/rustfs/rustfs"
|
||||
rust-version = "1.85"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
@@ -53,37 +53,37 @@ unsafe_code = "deny"
|
||||
all = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "0.0.1" }
|
||||
rustfs-appauth = { path = "crates/appauth", version = "0.0.1" }
|
||||
rustfs-common = { path = "crates/common", version = "0.0.1" }
|
||||
rustfs-crypto = { path = "crates/crypto", version = "0.0.1" }
|
||||
rustfs-ecstore = { path = "crates/ecstore", version = "0.0.1" }
|
||||
rustfs-iam = { path = "crates/iam", version = "0.0.1" }
|
||||
rustfs-lock = { path = "crates/lock", version = "0.0.1" }
|
||||
rustfs-madmin = { path = "crates/madmin", version = "0.0.1" }
|
||||
rustfs-policy = { path = "crates/policy", version = "0.0.1" }
|
||||
rustfs-protos = { path = "crates/protos", version = "0.0.1" }
|
||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "0.0.1" }
|
||||
rustfs = { path = "./rustfs", version = "0.0.1" }
|
||||
rustfs-zip = { path = "./crates/zip", version = "0.0.1" }
|
||||
rustfs-config = { path = "./crates/config", version = "0.0.1" }
|
||||
rustfs-obs = { path = "crates/obs", version = "0.0.1" }
|
||||
rustfs-notify = { path = "crates/notify", version = "0.0.1" }
|
||||
rustfs-utils = { path = "crates/utils", version = "0.0.1" }
|
||||
rustfs-rio = { path = "crates/rio", version = "0.0.1" }
|
||||
rustfs-filemeta = { path = "crates/filemeta", version = "0.0.1" }
|
||||
rustfs-signer = { path = "crates/signer", version = "0.0.1" }
|
||||
rustfs-workers = { path = "crates/workers", version = "0.0.1" }
|
||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "0.0.3" }
|
||||
rustfs-appauth = { path = "crates/appauth", version = "0.0.3" }
|
||||
rustfs-common = { path = "crates/common", version = "0.0.3" }
|
||||
rustfs-crypto = { path = "crates/crypto", version = "0.0.3" }
|
||||
rustfs-ecstore = { path = "crates/ecstore", version = "0.0.3" }
|
||||
rustfs-iam = { path = "crates/iam", version = "0.0.3" }
|
||||
rustfs-lock = { path = "crates/lock", version = "0.0.3" }
|
||||
rustfs-madmin = { path = "crates/madmin", version = "0.0.3" }
|
||||
rustfs-policy = { path = "crates/policy", version = "0.0.3" }
|
||||
rustfs-protos = { path = "crates/protos", version = "0.0.3" }
|
||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "0.0.3" }
|
||||
rustfs = { path = "./rustfs", version = "0.0.3" }
|
||||
rustfs-zip = { path = "./crates/zip", version = "0.0.3" }
|
||||
rustfs-config = { path = "./crates/config", version = "0.0.3" }
|
||||
rustfs-obs = { path = "crates/obs", version = "0.0.3" }
|
||||
rustfs-notify = { path = "crates/notify", version = "0.0.3" }
|
||||
rustfs-utils = { path = "crates/utils", version = "0.0.3" }
|
||||
rustfs-rio = { path = "crates/rio", version = "0.0.3" }
|
||||
rustfs-filemeta = { path = "crates/filemeta", version = "0.0.3" }
|
||||
rustfs-signer = { path = "crates/signer", version = "0.0.3" }
|
||||
rustfs-workers = { path = "crates/workers", version = "0.0.3" }
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
arc-swap = "1.7.1"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
atoi = "2.0.0"
|
||||
async-channel = "2.3.1"
|
||||
async-channel = "2.4.0"
|
||||
async-recursion = "1.1.1"
|
||||
async-trait = "0.1.88"
|
||||
async-compression = { version = "0.4.0" }
|
||||
atomic_enum = "0.3.0"
|
||||
aws-sdk-s3 = "1.95.0"
|
||||
aws-sdk-s3 = "1.96.0"
|
||||
axum = "0.8.4"
|
||||
axum-extra = "0.10.1"
|
||||
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
|
||||
@@ -107,7 +107,7 @@ dioxus = { version = "0.6.3", features = ["router"] }
|
||||
dirs = "6.0.0"
|
||||
enumset = "1.1.6"
|
||||
flatbuffers = "25.2.10"
|
||||
flate2 = "1.1.1"
|
||||
flate2 = "1.1.2"
|
||||
flexi_logger = { version = "0.31.2", features = ["trc", "dont_minimize_extra_stacks"] }
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = "0.3.31"
|
||||
@@ -124,7 +124,7 @@ hyper-util = { version = "0.1.14", features = [
|
||||
"server-auto",
|
||||
"server-graceful",
|
||||
] }
|
||||
hyper-rustls = "0.27.5"
|
||||
hyper-rustls = "0.27.7"
|
||||
http = "1.3.1"
|
||||
http-body = "1.0.1"
|
||||
humantime = "2.2.0"
|
||||
@@ -195,12 +195,12 @@ rmp-serde = "1.3.0"
|
||||
rsa = "0.9.8"
|
||||
rumqttc = { version = "0.24" }
|
||||
rust-embed = { version = "8.7.2" }
|
||||
rust-i18n = { version = "3.1.4" }
|
||||
rust-i18n = { version = "3.1.5" }
|
||||
rustfs-rsc = "2025.506.1"
|
||||
rustls = { version = "0.23.28" }
|
||||
rustls-pki-types = "1.12.0"
|
||||
rustls-pemfile = "2.2.0"
|
||||
s3s = { git = "https://github.com/Nugine/s3s.git", rev = "4733cdfb27b2713e832967232cbff413bb768c10" }
|
||||
s3s = { version = "0.12.0-minio-preview.1" }
|
||||
shadow-rs = { version = "1.2.0", default-features = false }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = { version = "1.0.140", features = ["raw_value"] }
|
||||
@@ -225,7 +225,7 @@ time = { version = "0.3.41", features = [
|
||||
"macros",
|
||||
"serde",
|
||||
] }
|
||||
tokio = { version = "1.45.1", features = ["fs", "rt-multi-thread"] }
|
||||
tokio = { version = "1.46.0", features = ["fs", "rt-multi-thread"] }
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17" }
|
||||
tokio-tar = "0.3.1"
|
||||
@@ -251,7 +251,7 @@ uuid = { version = "1.17.0", features = [
|
||||
wildmatch = { version = "2.4.0", features = ["serde"] }
|
||||
winapi = { version = "0.3.9" }
|
||||
xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] }
|
||||
zip = "2.2.0"
|
||||
zip = "2.4.2"
|
||||
zstd = "0.13.3"
|
||||
|
||||
[profile.wasm-dev]
|
||||
|
||||
+22
-20
@@ -12,36 +12,38 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM alpine:latest
|
||||
FROM alpine:3.18 AS builder
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
RUN apk add -U --no-cache \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
curl \
|
||||
bash \
|
||||
unzip
|
||||
|
||||
# Create rustfs user and group
|
||||
RUN addgroup -g 1000 rustfs && \
|
||||
adduser -D -s /bin/sh -u 1000 -G rustfs rustfs
|
||||
RUN curl -Lo /tmp/rustfs.zip https://dl.rustfs.com/artifacts/rustfs/rustfs-release-x86_64-unknown-linux-musl.latest.zip && \
|
||||
unzip /tmp/rustfs.zip -d /tmp && \
|
||||
mv /tmp/rustfs-release-x86_64-unknown-linux-musl/bin/rustfs /rustfs && \
|
||||
chmod +x /rustfs && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
# Create data directories
|
||||
RUN mkdir -p /data/rustfs && \
|
||||
chown -R rustfs:rustfs /data
|
||||
FROM alpine:3.18
|
||||
|
||||
# Copy binary based on target architecture
|
||||
COPY --chown=rustfs:rustfs \
|
||||
target/*/release/rustfs \
|
||||
/usr/local/bin/rustfs
|
||||
RUN apk add -U --no-cache \
|
||||
ca-certificates \
|
||||
bash
|
||||
|
||||
RUN chmod +x /usr/local/bin/rustfs
|
||||
COPY --from=builder /rustfs /usr/local/bin/rustfs
|
||||
|
||||
# Switch to non-root user
|
||||
USER rustfs
|
||||
ENV RUSTFS_ACCESS_KEY=rustfsadmin \
|
||||
RUSTFS_SECRET_KEY=rustfsadmin \
|
||||
RUSTFS_ADDRESS=":9000" \
|
||||
RUSTFS_CONSOLE_ADDRESS=":9001" \
|
||||
RUSTFS_CONSOLE_ENABLE=true \
|
||||
RUST_LOG=warn
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 9000 9001
|
||||
|
||||
RUN mkdir -p /data
|
||||
VOLUME /data
|
||||
|
||||
# Set default command
|
||||
CMD ["rustfs", "/data"]
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
[](https://rustfs.com)
|
||||
[](https://rustfs.com)
|
||||
|
||||
<p align="center">RustFS is a high-performance distributed object storage software built using Rust</p>
|
||||
|
||||
<p align="center">
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/docker.yml"><img alt="Build and Push Docker Images" src="https://github.com/rustfs/rustfs/actions/workflows/docker.yml/badge.svg" /></a>
|
||||
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/rustfs/rustfs"/>
|
||||
<img alt="Github Last Commit" src="https://img.shields.io/github/last-commit/rustfs/rustfs"/>
|
||||
<img alt="Github Contributors" src="https://img.shields.io/github/contributors/rustfs/rustfs"/>
|
||||
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/rustfs/rustfs"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1107178041848909847?label=discord"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -63,14 +61,20 @@ Stress test server parameters
|
||||
|
||||
To get started with RustFS, follow these steps:
|
||||
|
||||
1. **Install RustFS**: Download the latest release from our [GitHub Releases](https://github.com/rustfs/rustfs/releases).
|
||||
2. **Run RustFS**: Use the provided binary to start the server.
|
||||
1. **One-click installation script (Option 1)**
|
||||
|
||||
```bash
|
||||
./rustfs /data
|
||||
curl -O https://rustfs.com/install_rustfs.sh && bash install_rustfs.sh
|
||||
```
|
||||
|
||||
3. **Access the Console**: Open your web browser and navigate to `http://localhost:9001` to access the RustFS console.
|
||||
2. **Docker Quick Start (Option 2)**
|
||||
|
||||
```bash
|
||||
podman run -d -p 9000:9000 -p 9001:9001 -v /data:/data quay.io/rustfs/rustfs
|
||||
```
|
||||
|
||||
|
||||
3. **Access the Console**: Open your web browser and navigate to `http://localhost:9001` to access the RustFS console, default username and password is `rustfsadmin` .
|
||||
4. **Create a Bucket**: Use the console to create a new bucket for your objects.
|
||||
5. **Upload Objects**: You can upload files directly through the console or use S3-compatible APIs to interact with your RustFS instance.
|
||||
|
||||
|
||||
+13
-9
@@ -1,14 +1,12 @@
|
||||
[](https://rustfs.com)
|
||||
[](https://rustfs.com)
|
||||
|
||||
<p align="center">RustFS 是一个使用 Rust 构建的高性能分布式对象存储软件</p >
|
||||
|
||||
<p align="center">
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/docker.yml"><img alt="Build and Push Docker Images" src="https://github.com/rustfs/rustfs/actions/workflows/docker.yml/badge.svg" /></a>
|
||||
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/rustfs/rustfs"/>
|
||||
<img alt="Github Last Commit" src="https://img.shields.io/github/last-commit/rustfs/rustfs"/>
|
||||
<img alt="Github Contributors" src="https://img.shields.io/github/contributors/rustfs/rustfs"/>
|
||||
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/rustfs/rustfs"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1107178041848909847?label=discord"/>
|
||||
</p >
|
||||
|
||||
<p align="center">
|
||||
@@ -63,14 +61,20 @@ RustFS 是一个使用 Rust(全球最受欢迎的编程语言之一)构建
|
||||
|
||||
要开始使用 RustFS,请按照以下步骤操作:
|
||||
|
||||
1. **安装 RustFS**:从我们的 [GitHub Releases](https://github.com/rustfs/rustfs/releases) 下载最新版本。
|
||||
2. **运行 RustFS**:使用提供的二进制文件启动服务器。
|
||||
1. **一键脚本快速启动 (方案一)**
|
||||
|
||||
```bash
|
||||
./rustfs /data
|
||||
curl -O https://rustfs.com/install_rustfs.sh && bash install_rustfs.sh
|
||||
```
|
||||
|
||||
3. **访问控制台**:打开 Web 浏览器并导航到 `http://localhost:9001` 以访问 RustFS 控制台。
|
||||
2. **Docker快速启动(方案二)**
|
||||
|
||||
```bash
|
||||
podman run -d -p 9000:9000 -p 9001:9001 -v /data:/data quay.io/rustfs/rustfs
|
||||
```
|
||||
|
||||
|
||||
3. **访问控制台**:打开 Web 浏览器并导航到 `http://localhost:9001` 以访问 RustFS 控制台,默认的用户名和密码是 `rustfsadmin` 。
|
||||
4. **创建存储桶**:使用控制台为您的对象创建新的存储桶。
|
||||
5. **上传对象**:您可以直接通过控制台上传文件,或使用 S3 兼容的 API 与您的 RustFS 实例交互。
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# TODO LIST
|
||||
|
||||
## 基础存储
|
||||
|
||||
- [x] EC 可用读写数量判断 Read/WriteQuorum
|
||||
- [ ] 优化后台并发执行,可中断,传引用?
|
||||
- [x] 小文件存储到 metafile, inlinedata
|
||||
- [x] 完善 bucketmeta
|
||||
- [x] 对象锁
|
||||
- [x] 边读写边 hash,实现 reader 嵌套
|
||||
- [x] 远程 rpc
|
||||
- [x] 错误类型判断,程序中判断错误类型,如何统一错误
|
||||
- [x] 优化 xlmeta, 自定义 msg 数据结构
|
||||
- [ ] 优化 io.reader 参考 GetObjectNInfo 方便 io copy 如果 异步写,再平衡
|
||||
- [ ] 代码优化 使用范型?
|
||||
- [ ] 抽象出 metafile 存储
|
||||
|
||||
## 基础功能
|
||||
|
||||
- [ ] 桶操作
|
||||
- [x] 创建 CreateBucket
|
||||
- [x] 列表 ListBuckets
|
||||
- [ ] 桶下面的文件列表 ListObjects
|
||||
- [x] 简单实现功能
|
||||
- [ ] 优化并发读取
|
||||
- [ ] 删除
|
||||
- [x] 详情 HeadBucket
|
||||
- [ ] 文件操作
|
||||
- [x] 上传 PutObject
|
||||
- [x] 大文件上传
|
||||
- [x] 创建分片上传 CreateMultipartUpload
|
||||
- [x] 上传分片 PubObjectPart
|
||||
- [x] 提交完成 CompleteMultipartUpload
|
||||
- [x] 取消上传 AbortMultipartUpload
|
||||
- [x] 下载 GetObject
|
||||
- [x] 删除 DeleteObjects
|
||||
- [ ] 版本控制
|
||||
- [ ] 对象锁
|
||||
- [ ] 复制 CopyObject
|
||||
- [ ] 详情 HeadObject
|
||||
- [ ] 对象预先签名(get、put、head、post)
|
||||
|
||||
## 扩展功能
|
||||
|
||||
- [ ] 用户管理
|
||||
- [ ] Policy 管理
|
||||
- [ ] AK/SK分配管理
|
||||
- [ ] data scanner 统计和对象修复
|
||||
- [ ] 桶配额
|
||||
- [ ] 桶只读
|
||||
- [ ] 桶复制
|
||||
- [ ] 桶事件通知
|
||||
- [ ] 桶公开、桶私有
|
||||
- [ ] 对象生命周期管理
|
||||
- [ ] prometheus 对接
|
||||
- [ ] 日志收集和日志外发
|
||||
- [ ] 对象压缩
|
||||
- [ ] STS
|
||||
- [ ] 分层(阿里云、腾讯云、S3 远程对接)
|
||||
|
||||
|
||||
|
||||
## 性能优化
|
||||
- [ ] bitrot impl AsyncRead/AsyncWrite
|
||||
- [ ] erasure 并发读写
|
||||
- [x] 完善删除逻辑,并发处理,先移动到回收站,
|
||||
- [ ] 空间不足时清空回收站
|
||||
- [ ] list_object 使用 reader 传输
|
||||
@@ -0,0 +1,477 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS AppAuth - Application Authentication
|
||||
|
||||
<p align="center">
|
||||
<strong>Secure application authentication and authorization for RustFS object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS AppAuth** provides secure application authentication and authorization mechanisms for the [RustFS](https://rustfs.com) distributed object storage system. It implements modern cryptographic standards including RSA-based authentication, JWT tokens, and secure session management for application-level access control.
|
||||
|
||||
> **Note:** This is a security-critical submodule of RustFS that provides essential application authentication capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔐 Authentication Methods
|
||||
|
||||
- **RSA Authentication**: Public-key cryptography for secure authentication
|
||||
- **JWT Tokens**: JSON Web Token support for stateless authentication
|
||||
- **API Keys**: Simple API key-based authentication
|
||||
- **Session Management**: Secure session handling and lifecycle management
|
||||
|
||||
### 🛡️ Security Features
|
||||
|
||||
- **Cryptographic Signing**: RSA digital signatures for request validation
|
||||
- **Token Encryption**: Encrypted token storage and transmission
|
||||
- **Key Rotation**: Automatic key rotation and management
|
||||
- **Audit Logging**: Comprehensive authentication event logging
|
||||
|
||||
### 🚀 Performance Features
|
||||
|
||||
- **Base64 Optimization**: High-performance base64 encoding/decoding
|
||||
- **Token Caching**: Efficient token validation caching
|
||||
- **Parallel Verification**: Concurrent authentication processing
|
||||
- **Hardware Acceleration**: Leverage CPU crypto extensions
|
||||
|
||||
### 🔧 Integration Features
|
||||
|
||||
- **S3 Compatibility**: AWS S3-compatible authentication
|
||||
- **Multi-Tenant**: Support for multiple application tenants
|
||||
- **Permission Mapping**: Fine-grained permission assignment
|
||||
- **External Integration**: LDAP, OAuth, and custom authentication providers
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-appauth = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Authentication Setup
|
||||
|
||||
```rust
|
||||
use rustfs_appauth::{AppAuthenticator, AuthConfig, AuthMethod};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Configure authentication
|
||||
let config = AuthConfig {
|
||||
auth_method: AuthMethod::RSA,
|
||||
key_size: 2048,
|
||||
token_expiry: Duration::from_hours(24),
|
||||
enable_caching: true,
|
||||
audit_logging: true,
|
||||
};
|
||||
|
||||
// Initialize authenticator
|
||||
let authenticator = AppAuthenticator::new(config).await?;
|
||||
|
||||
// Generate application credentials
|
||||
let app_credentials = authenticator.generate_app_credentials("my-app").await?;
|
||||
|
||||
println!("App ID: {}", app_credentials.app_id);
|
||||
println!("Public Key: {}", app_credentials.public_key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### RSA-Based Authentication
|
||||
|
||||
```rust
|
||||
use rustfs_appauth::{RSAAuthenticator, AuthRequest, AuthResponse};
|
||||
|
||||
async fn rsa_authentication_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create RSA authenticator
|
||||
let rsa_auth = RSAAuthenticator::new(2048).await?;
|
||||
|
||||
// Generate key pair for application
|
||||
let (private_key, public_key) = rsa_auth.generate_keypair().await?;
|
||||
|
||||
// Register application
|
||||
let app_id = rsa_auth.register_application("my-storage-app", &public_key).await?;
|
||||
println!("Application registered with ID: {}", app_id);
|
||||
|
||||
// Create authentication request
|
||||
let auth_request = AuthRequest {
|
||||
app_id: app_id.clone(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
request_data: b"GET /bucket/object".to_vec(),
|
||||
};
|
||||
|
||||
// Sign request with private key
|
||||
let signed_request = rsa_auth.sign_request(&auth_request, &private_key).await?;
|
||||
|
||||
// Verify authentication
|
||||
let auth_response = rsa_auth.authenticate(&signed_request).await?;
|
||||
|
||||
match auth_response {
|
||||
AuthResponse::Success { session_token, permissions } => {
|
||||
println!("Authentication successful!");
|
||||
println!("Session token: {}", session_token);
|
||||
println!("Permissions: {:?}", permissions);
|
||||
}
|
||||
AuthResponse::Failed { reason } => {
|
||||
println!("Authentication failed: {}", reason);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### JWT Token Management
|
||||
|
||||
```rust
|
||||
use rustfs_appauth::{JWTManager, TokenClaims, TokenRequest};
|
||||
|
||||
async fn jwt_management_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create JWT manager
|
||||
let jwt_manager = JWTManager::new("your-secret-key").await?;
|
||||
|
||||
// Create token claims
|
||||
let claims = TokenClaims {
|
||||
app_id: "my-app".to_string(),
|
||||
user_id: Some("user123".to_string()),
|
||||
permissions: vec![
|
||||
"read:bucket".to_string(),
|
||||
"write:bucket".to_string(),
|
||||
],
|
||||
expires_at: chrono::Utc::now() + chrono::Duration::hours(24),
|
||||
issued_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
// Generate JWT token
|
||||
let token = jwt_manager.generate_token(&claims).await?;
|
||||
println!("Generated token: {}", token);
|
||||
|
||||
// Validate token
|
||||
let validation_result = jwt_manager.validate_token(&token).await?;
|
||||
|
||||
match validation_result {
|
||||
Ok(validated_claims) => {
|
||||
println!("Token valid for app: {}", validated_claims.app_id);
|
||||
println!("Permissions: {:?}", validated_claims.permissions);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Token validation failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh token
|
||||
let refreshed_token = jwt_manager.refresh_token(&token).await?;
|
||||
println!("Refreshed token: {}", refreshed_token);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### API Key Authentication
|
||||
|
||||
```rust
|
||||
use rustfs_appauth::{APIKeyManager, APIKeyConfig, KeyPermissions};
|
||||
|
||||
async fn api_key_authentication() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api_key_manager = APIKeyManager::new().await?;
|
||||
|
||||
// Create API key configuration
|
||||
let key_config = APIKeyConfig {
|
||||
app_name: "storage-client".to_string(),
|
||||
permissions: KeyPermissions {
|
||||
read_buckets: vec!["public-*".to_string()],
|
||||
write_buckets: vec!["uploads".to_string()],
|
||||
admin_access: false,
|
||||
},
|
||||
expires_at: Some(chrono::Utc::now() + chrono::Duration::days(90)),
|
||||
rate_limit: Some(1000), // requests per hour
|
||||
};
|
||||
|
||||
// Generate API key
|
||||
let api_key = api_key_manager.generate_key(&key_config).await?;
|
||||
println!("Generated API key: {}", api_key.key);
|
||||
println!("Key ID: {}", api_key.key_id);
|
||||
|
||||
// Authenticate with API key
|
||||
let auth_result = api_key_manager.authenticate(&api_key.key).await?;
|
||||
|
||||
if auth_result.is_valid {
|
||||
println!("API key authentication successful");
|
||||
println!("Rate limit remaining: {}", auth_result.rate_limit_remaining);
|
||||
}
|
||||
|
||||
// List API keys for application
|
||||
let keys = api_key_manager.list_keys("storage-client").await?;
|
||||
for key in keys {
|
||||
println!("Key: {} - Status: {} - Expires: {:?}",
|
||||
key.key_id, key.status, key.expires_at);
|
||||
}
|
||||
|
||||
// Revoke API key
|
||||
api_key_manager.revoke_key(&api_key.key_id).await?;
|
||||
println!("API key revoked successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
```rust
|
||||
use rustfs_appauth::{SessionManager, SessionConfig, SessionInfo};
|
||||
|
||||
async fn session_management_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Configure session management
|
||||
let session_config = SessionConfig {
|
||||
session_timeout: Duration::from_hours(8),
|
||||
max_sessions_per_app: 10,
|
||||
require_refresh: true,
|
||||
secure_cookies: true,
|
||||
};
|
||||
|
||||
let session_manager = SessionManager::new(session_config).await?;
|
||||
|
||||
// Create new session
|
||||
let session_info = SessionInfo {
|
||||
app_id: "web-app".to_string(),
|
||||
user_id: Some("user456".to_string()),
|
||||
ip_address: "192.168.1.100".to_string(),
|
||||
user_agent: "RustFS-Client/1.0".to_string(),
|
||||
};
|
||||
|
||||
let session = session_manager.create_session(&session_info).await?;
|
||||
println!("Session created: {}", session.session_id);
|
||||
|
||||
// Validate session
|
||||
let validation = session_manager.validate_session(&session.session_id).await?;
|
||||
|
||||
if validation.is_valid {
|
||||
println!("Session is valid, expires at: {}", validation.expires_at);
|
||||
}
|
||||
|
||||
// Refresh session
|
||||
session_manager.refresh_session(&session.session_id).await?;
|
||||
println!("Session refreshed");
|
||||
|
||||
// Get active sessions
|
||||
let active_sessions = session_manager.get_active_sessions("web-app").await?;
|
||||
println!("Active sessions: {}", active_sessions.len());
|
||||
|
||||
// Terminate session
|
||||
session_manager.terminate_session(&session.session_id).await?;
|
||||
println!("Session terminated");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Tenant Authentication
|
||||
|
||||
```rust
|
||||
use rustfs_appauth::{MultiTenantAuth, TenantConfig, TenantPermissions};
|
||||
|
||||
async fn multi_tenant_auth_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let multi_tenant_auth = MultiTenantAuth::new().await?;
|
||||
|
||||
// Create tenant configurations
|
||||
let tenant1_config = TenantConfig {
|
||||
tenant_id: "company-a".to_string(),
|
||||
name: "Company A".to_string(),
|
||||
permissions: TenantPermissions {
|
||||
max_buckets: 100,
|
||||
max_storage_gb: 1000,
|
||||
allowed_regions: vec!["us-east-1".to_string(), "us-west-2".to_string()],
|
||||
},
|
||||
auth_methods: vec![AuthMethod::RSA, AuthMethod::JWT],
|
||||
};
|
||||
|
||||
let tenant2_config = TenantConfig {
|
||||
tenant_id: "company-b".to_string(),
|
||||
name: "Company B".to_string(),
|
||||
permissions: TenantPermissions {
|
||||
max_buckets: 50,
|
||||
max_storage_gb: 500,
|
||||
allowed_regions: vec!["eu-west-1".to_string()],
|
||||
},
|
||||
auth_methods: vec![AuthMethod::APIKey],
|
||||
};
|
||||
|
||||
// Register tenants
|
||||
multi_tenant_auth.register_tenant(&tenant1_config).await?;
|
||||
multi_tenant_auth.register_tenant(&tenant2_config).await?;
|
||||
|
||||
// Authenticate application for specific tenant
|
||||
let auth_request = TenantAuthRequest {
|
||||
tenant_id: "company-a".to_string(),
|
||||
app_id: "app-1".to_string(),
|
||||
credentials: AuthCredentials::RSA {
|
||||
signature: "signed-data".to_string(),
|
||||
public_key: "public-key-data".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let auth_result = multi_tenant_auth.authenticate(&auth_request).await?;
|
||||
|
||||
if auth_result.is_authenticated {
|
||||
println!("Multi-tenant authentication successful");
|
||||
println!("Tenant: {}", auth_result.tenant_id);
|
||||
println!("Permissions: {:?}", auth_result.permissions);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Middleware
|
||||
|
||||
```rust
|
||||
use rustfs_appauth::{AuthMiddleware, AuthContext, MiddlewareConfig};
|
||||
use axum::{Router, middleware, Extension};
|
||||
|
||||
async fn setup_auth_middleware() -> Result<Router, Box<dyn std::error::Error>> {
|
||||
// Configure authentication middleware
|
||||
let middleware_config = MiddlewareConfig {
|
||||
skip_paths: vec!["/health".to_string(), "/metrics".to_string()],
|
||||
require_auth: true,
|
||||
audit_requests: true,
|
||||
};
|
||||
|
||||
let auth_middleware = AuthMiddleware::new(middleware_config).await?;
|
||||
|
||||
// Create router with authentication middleware
|
||||
let app = Router::new()
|
||||
.route("/api/buckets", axum::routing::get(list_buckets))
|
||||
.route("/api/objects", axum::routing::post(upload_object))
|
||||
.layer(middleware::from_fn(auth_middleware.authenticate))
|
||||
.layer(Extension(auth_middleware));
|
||||
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
async fn list_buckets(
|
||||
Extension(auth_context): Extension<AuthContext>,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Use authentication context
|
||||
println!("Authenticated app: {}", auth_context.app_id);
|
||||
println!("Permissions: {:?}", auth_context.permissions);
|
||||
|
||||
// Your bucket listing logic here
|
||||
Ok("Bucket list".to_string())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### AppAuth Architecture
|
||||
|
||||
```
|
||||
AppAuth Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Authentication API │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ RSA Auth │ JWT Tokens │ API Keys │ Sessions │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Cryptographic Operations │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Signing/ │ Token │ Key │ Session │
|
||||
│ Verification │ Management │ Management │ Storage │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Security Infrastructure │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
| Method | Security Level | Use Case | Performance |
|
||||
|--------|----------------|----------|-------------|
|
||||
| RSA | High | Enterprise applications | Medium |
|
||||
| JWT | Medium-High | Web applications | High |
|
||||
| API Key | Medium | Service-to-service | Very High |
|
||||
| Session | Medium | Interactive applications | High |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test RSA authentication
|
||||
cargo test rsa_auth
|
||||
|
||||
# Test JWT tokens
|
||||
cargo test jwt_tokens
|
||||
|
||||
# Test API key management
|
||||
cargo test api_keys
|
||||
|
||||
# Test session management
|
||||
cargo test sessions
|
||||
|
||||
# Integration tests
|
||||
cargo test --test integration
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Dependencies**: RSA cryptographic libraries
|
||||
- **Security**: Secure key storage recommended
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS IAM](../iam) - Identity and access management
|
||||
- [RustFS Signer](../signer) - Request signing
|
||||
- [RustFS Crypto](../crypto) - Cryptographic operations
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [AppAuth API Reference](https://docs.rustfs.com/appauth/)
|
||||
- [Security Guide](https://docs.rustfs.com/security/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🔐 by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,295 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Common - Shared Components
|
||||
|
||||
<p align="center">
|
||||
<strong>Common types, utilities, and shared components for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Common** provides shared components, types, and utilities used across all RustFS modules. This foundational library ensures consistency, reduces code duplication, and provides essential building blocks for the [RustFS](https://rustfs.com) distributed object storage system.
|
||||
|
||||
> **Note:** This is a foundational submodule of RustFS that provides essential shared components for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔧 Core Types
|
||||
|
||||
- **Common Data Structures**: Shared types and enums
|
||||
- **Error Handling**: Unified error types and utilities
|
||||
- **Result Types**: Consistent result handling patterns
|
||||
- **Constants**: System-wide constants and defaults
|
||||
|
||||
### 🛠️ Utilities
|
||||
|
||||
- **Async Helpers**: Common async patterns and utilities
|
||||
- **Serialization**: Shared serialization utilities
|
||||
- **Logging**: Common logging and tracing setup
|
||||
- **Metrics**: Shared metrics and observability
|
||||
|
||||
### 🌐 Network Components
|
||||
|
||||
- **gRPC Common**: Shared gRPC types and utilities
|
||||
- **Protocol Helpers**: Common protocol implementations
|
||||
- **Connection Management**: Shared connection utilities
|
||||
- **Request/Response Types**: Common API types
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-common = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Common Types
|
||||
|
||||
```rust
|
||||
use rustfs_common::{Result, Error, ObjectInfo, BucketInfo};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Use common result type
|
||||
let result = some_operation()?;
|
||||
|
||||
// Use common object info
|
||||
let object = ObjectInfo {
|
||||
name: "example.txt".to_string(),
|
||||
size: 1024,
|
||||
etag: "d41d8cd98f00b204e9800998ecf8427e".to_string(),
|
||||
last_modified: chrono::Utc::now(),
|
||||
content_type: "text/plain".to_string(),
|
||||
};
|
||||
|
||||
println!("Object: {} ({} bytes)", object.name, object.size);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```rust
|
||||
use rustfs_common::{Error, ErrorKind, Result};
|
||||
|
||||
fn example_operation() -> Result<String> {
|
||||
// Different error types
|
||||
match some_condition {
|
||||
true => Ok("Success".to_string()),
|
||||
false => Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Invalid operation parameters"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_errors() {
|
||||
match example_operation() {
|
||||
Ok(value) => println!("Success: {}", value),
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
ErrorKind::InvalidInput => println!("Input error: {}", e),
|
||||
ErrorKind::NotFound => println!("Not found: {}", e),
|
||||
ErrorKind::PermissionDenied => println!("Access denied: {}", e),
|
||||
_ => println!("Other error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Utilities
|
||||
|
||||
```rust
|
||||
use rustfs_common::async_utils::{timeout_with_default, retry_with_backoff, spawn_task};
|
||||
use std::time::Duration;
|
||||
|
||||
async fn async_operations() -> Result<()> {
|
||||
// Timeout with default value
|
||||
let result = timeout_with_default(
|
||||
Duration::from_secs(5),
|
||||
expensive_operation(),
|
||||
"default_value".to_string()
|
||||
).await;
|
||||
|
||||
// Retry with exponential backoff
|
||||
let result = retry_with_backoff(
|
||||
3, // max attempts
|
||||
Duration::from_millis(100), // initial delay
|
||||
|| async { fallible_operation().await }
|
||||
).await?;
|
||||
|
||||
// Spawn background task
|
||||
spawn_task("background-worker", async {
|
||||
background_work().await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Metrics and Observability
|
||||
|
||||
```rust
|
||||
use rustfs_common::metrics::{Counter, Histogram, Gauge, MetricsRegistry};
|
||||
|
||||
fn setup_metrics() -> Result<()> {
|
||||
let registry = MetricsRegistry::new();
|
||||
|
||||
// Create metrics
|
||||
let requests_total = Counter::new("requests_total", "Total number of requests")?;
|
||||
let request_duration = Histogram::new(
|
||||
"request_duration_seconds",
|
||||
"Request duration in seconds"
|
||||
)?;
|
||||
let active_connections = Gauge::new(
|
||||
"active_connections",
|
||||
"Number of active connections"
|
||||
)?;
|
||||
|
||||
// Register metrics
|
||||
registry.register(Box::new(requests_total))?;
|
||||
registry.register(Box::new(request_duration))?;
|
||||
registry.register(Box::new(active_connections))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### gRPC Common Types
|
||||
|
||||
```rust
|
||||
use rustfs_common::grpc::{GrpcResult, GrpcError, TonicStatus};
|
||||
use tonic::{Request, Response, Status};
|
||||
|
||||
async fn grpc_service_example(
|
||||
request: Request<MyRequest>
|
||||
) -> GrpcResult<MyResponse> {
|
||||
let req = request.into_inner();
|
||||
|
||||
// Validate request
|
||||
if req.name.is_empty() {
|
||||
return Err(GrpcError::invalid_argument("Name cannot be empty"));
|
||||
}
|
||||
|
||||
// Process request
|
||||
let response = MyResponse {
|
||||
result: format!("Processed: {}", req.name),
|
||||
status: "success".to_string(),
|
||||
};
|
||||
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
// Error conversion
|
||||
impl From<Error> for Status {
|
||||
fn from(err: Error) -> Self {
|
||||
match err.kind() {
|
||||
ErrorKind::NotFound => Status::not_found(err.to_string()),
|
||||
ErrorKind::PermissionDenied => Status::permission_denied(err.to_string()),
|
||||
ErrorKind::InvalidInput => Status::invalid_argument(err.to_string()),
|
||||
_ => Status::internal(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Common Module Structure
|
||||
|
||||
```
|
||||
Common Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Public API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Core Types │ Error Types │ Result Types │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Async Utils │ Metrics │ gRPC Common │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Constants │ Serialization │ Logging │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Foundation Types │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Core Components
|
||||
|
||||
| Component | Purpose | Usage |
|
||||
|-----------|---------|-------|
|
||||
| Types | Common data structures | Shared across all modules |
|
||||
| Errors | Unified error handling | Consistent error reporting |
|
||||
| Async Utils | Async patterns | Common async operations |
|
||||
| Metrics | Observability | Performance monitoring |
|
||||
| gRPC | Protocol support | Service communication |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test specific components
|
||||
cargo test types
|
||||
cargo test errors
|
||||
cargo test async_utils
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Dependencies**: Minimal, focused on essential functionality
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Utils](../utils) - Utility functions
|
||||
- [RustFS Config](../config) - Configuration management
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Common API Reference](https://docs.rustfs.com/common/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🔧 by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,404 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Config - Configuration Management
|
||||
|
||||
<p align="center">
|
||||
<strong>Centralized configuration management for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Config** is the configuration management module for the [RustFS](https://rustfs.com) distributed object storage system. It provides centralized configuration handling, environment-based configuration loading, validation, and runtime configuration updates for all RustFS components.
|
||||
|
||||
> **Note:** This is a foundational submodule of RustFS that provides essential configuration management capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### ⚙️ Configuration Management
|
||||
|
||||
- **Multi-Format Support**: JSON, YAML, TOML configuration formats
|
||||
- **Environment Variables**: Automatic environment variable override
|
||||
- **Default Values**: Comprehensive default configuration
|
||||
- **Validation**: Configuration validation and error reporting
|
||||
|
||||
### 🔧 Advanced Features
|
||||
|
||||
- **Hot Reload**: Runtime configuration updates without restart
|
||||
- **Profile Support**: Environment-specific configuration profiles
|
||||
- **Secret Management**: Secure handling of sensitive configuration
|
||||
- **Configuration Merging**: Hierarchical configuration composition
|
||||
|
||||
### 🛠️ Developer Features
|
||||
|
||||
- **Type Safety**: Strongly typed configuration structures
|
||||
- **Documentation**: Auto-generated configuration documentation
|
||||
- **CLI Integration**: Command-line configuration override
|
||||
- **Testing Support**: Configuration mocking for tests
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-config = "0.1.0"
|
||||
|
||||
# With specific features
|
||||
rustfs-config = { version = "0.1.0", features = ["constants", "notify"] }
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
|
||||
Available features:
|
||||
|
||||
- `constants` - Configuration constants and compile-time values
|
||||
- `notify` - Configuration change notification support
|
||||
- `observability` - Observability and metrics configuration
|
||||
- `default` - Core configuration functionality
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Configuration Loading
|
||||
|
||||
```rust
|
||||
use rustfs_config::{Config, ConfigBuilder, ConfigFormat};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Load configuration from file
|
||||
let config = Config::from_file("config.yaml")?;
|
||||
|
||||
// Load with environment overrides
|
||||
let config = ConfigBuilder::new()
|
||||
.add_file("config.yaml")
|
||||
.add_env_prefix("RUSTFS")
|
||||
.build()?;
|
||||
|
||||
// Access configuration values
|
||||
println!("Server address: {}", config.server.address);
|
||||
println!("Storage path: {}", config.storage.path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Environment-Based Configuration
|
||||
|
||||
```rust
|
||||
use rustfs_config::{Config, Environment};
|
||||
|
||||
async fn load_environment_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Load configuration based on environment
|
||||
let env = Environment::detect()?;
|
||||
let config = Config::for_environment(env).await?;
|
||||
|
||||
match env {
|
||||
Environment::Development => {
|
||||
println!("Using development configuration");
|
||||
println!("Debug mode: {}", config.debug.enabled);
|
||||
}
|
||||
Environment::Production => {
|
||||
println!("Using production configuration");
|
||||
println!("Log level: {}", config.logging.level);
|
||||
}
|
||||
Environment::Testing => {
|
||||
println!("Using test configuration");
|
||||
println!("Test database: {}", config.database.test_url);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Structure
|
||||
|
||||
```rust
|
||||
use rustfs_config::{Config, ServerConfig, StorageConfig, SecurityConfig};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ApplicationConfig {
|
||||
pub server: ServerConfig,
|
||||
pub storage: StorageConfig,
|
||||
pub security: SecurityConfig,
|
||||
pub logging: LoggingConfig,
|
||||
pub monitoring: MonitoringConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ServerConfig {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub workers: usize,
|
||||
pub timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct StorageConfig {
|
||||
pub path: String,
|
||||
pub max_size: u64,
|
||||
pub compression: bool,
|
||||
pub erasure_coding: ErasureCodingConfig,
|
||||
}
|
||||
|
||||
fn load_typed_config() -> Result<ApplicationConfig, Box<dyn std::error::Error>> {
|
||||
let config: ApplicationConfig = Config::builder()
|
||||
.add_file("config.yaml")
|
||||
.add_env_prefix("RUSTFS")
|
||||
.set_default("server.port", 9000)?
|
||||
.set_default("server.workers", 4)?
|
||||
.build_typed()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Validation
|
||||
|
||||
```rust
|
||||
use rustfs_config::{Config, ValidationError, Validator};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConfigValidator;
|
||||
|
||||
impl Validator<ApplicationConfig> for ConfigValidator {
|
||||
fn validate(&self, config: &ApplicationConfig) -> Result<(), ValidationError> {
|
||||
// Validate server configuration
|
||||
if config.server.port < 1024 {
|
||||
return Err(ValidationError::new("server.port", "Port must be >= 1024"));
|
||||
}
|
||||
|
||||
if config.server.workers == 0 {
|
||||
return Err(ValidationError::new("server.workers", "Workers must be > 0"));
|
||||
}
|
||||
|
||||
// Validate storage configuration
|
||||
if !std::path::Path::new(&config.storage.path).exists() {
|
||||
return Err(ValidationError::new("storage.path", "Storage path does not exist"));
|
||||
}
|
||||
|
||||
// Validate erasure coding parameters
|
||||
if config.storage.erasure_coding.data_drives + config.storage.erasure_coding.parity_drives > 16 {
|
||||
return Err(ValidationError::new("storage.erasure_coding", "Total drives cannot exceed 16"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_configuration() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config: ApplicationConfig = Config::load_with_validation(
|
||||
"config.yaml",
|
||||
ConfigValidator,
|
||||
)?;
|
||||
|
||||
println!("Configuration is valid!");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Hot Configuration Reload
|
||||
|
||||
```rust
|
||||
use rustfs_config::{ConfigWatcher, ConfigEvent};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
async fn watch_configuration_changes() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (tx, mut rx) = mpsc::channel::<ConfigEvent>(100);
|
||||
|
||||
// Start configuration watcher
|
||||
let watcher = ConfigWatcher::new("config.yaml", tx)?;
|
||||
watcher.start().await?;
|
||||
|
||||
// Handle configuration changes
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
ConfigEvent::Changed(new_config) => {
|
||||
println!("Configuration changed, reloading...");
|
||||
// Apply new configuration
|
||||
apply_configuration(new_config).await?;
|
||||
}
|
||||
ConfigEvent::Error(err) => {
|
||||
eprintln!("Configuration error: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_configuration(config: ApplicationConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Update server configuration
|
||||
// Update storage configuration
|
||||
// Update security settings
|
||||
// etc.
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Profiles
|
||||
|
||||
```rust
|
||||
use rustfs_config::{Config, Profile, ProfileManager};
|
||||
|
||||
fn load_profile_based_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let profile_manager = ProfileManager::new("configs/")?;
|
||||
|
||||
// Load specific profile
|
||||
let config = profile_manager.load_profile("production")?;
|
||||
|
||||
// Load with fallback
|
||||
let config = profile_manager
|
||||
.load_profile("staging")
|
||||
.or_else(|_| profile_manager.load_profile("default"))?;
|
||||
|
||||
// Merge multiple profiles
|
||||
let config = profile_manager
|
||||
.merge_profiles(&["base", "production", "regional"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Configuration Architecture
|
||||
|
||||
```
|
||||
Config Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Configuration API │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ File Loader │ Env Loader │ CLI Parser │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Configuration Merger │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Validation │ Watching │ Hot Reload │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Type System Integration │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Configuration Sources
|
||||
|
||||
| Source | Priority | Format | Example |
|
||||
|--------|----------|---------|---------|
|
||||
| Command Line | 1 (Highest) | Key-Value | `--server.port=8080` |
|
||||
| Environment Variables | 2 | Key-Value | `RUSTFS_SERVER_PORT=8080` |
|
||||
| Configuration File | 3 | JSON/YAML/TOML | `config.yaml` |
|
||||
| Default Values | 4 (Lowest) | Code | Compile-time defaults |
|
||||
|
||||
## 📋 Configuration Reference
|
||||
|
||||
### Server Configuration
|
||||
|
||||
```yaml
|
||||
server:
|
||||
address: "0.0.0.0"
|
||||
port: 9000
|
||||
workers: 4
|
||||
timeout: "30s"
|
||||
tls:
|
||||
enabled: true
|
||||
cert_file: "/etc/ssl/server.crt"
|
||||
key_file: "/etc/ssl/server.key"
|
||||
```
|
||||
|
||||
### Storage Configuration
|
||||
|
||||
```yaml
|
||||
storage:
|
||||
path: "/var/lib/rustfs"
|
||||
max_size: "1TB"
|
||||
compression: true
|
||||
erasure_coding:
|
||||
data_drives: 8
|
||||
parity_drives: 4
|
||||
stripe_size: "1MB"
|
||||
```
|
||||
|
||||
### Security Configuration
|
||||
|
||||
```yaml
|
||||
security:
|
||||
auth:
|
||||
enabled: true
|
||||
method: "jwt"
|
||||
secret_key: "${JWT_SECRET}"
|
||||
encryption:
|
||||
algorithm: "AES-256-GCM"
|
||||
key_rotation_interval: "24h"
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test configuration loading
|
||||
cargo test config_loading
|
||||
|
||||
# Test validation
|
||||
cargo test validation
|
||||
|
||||
# Test hot reload
|
||||
cargo test hot_reload
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Dependencies**: Minimal external dependencies
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Utils](../utils) - Utility functions
|
||||
- [RustFS Common](../common) - Common types and utilities
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Config API Reference](https://docs.rustfs.com/config/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with ⚙️ by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,329 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Crypto Module
|
||||
|
||||
<p align="center">
|
||||
<strong>High-performance cryptographic module for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
The **RustFS Crypto Module** is a core cryptographic component of the [RustFS](https://rustfs.com) distributed object storage system. This module provides secure, high-performance encryption and decryption capabilities, JWT token management, and cross-platform cryptographic operations designed specifically for enterprise-grade storage systems.
|
||||
|
||||
> **Note:** This is a submodule of RustFS and is designed to work seamlessly within the RustFS ecosystem. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔐 Encryption & Decryption
|
||||
|
||||
- **Multiple Algorithms**: Support for AES-GCM, ChaCha20Poly1305, and PBKDF2
|
||||
- **Key Derivation**: Argon2id and PBKDF2 for secure key generation
|
||||
- **Memory Safety**: Built with Rust's memory safety guarantees
|
||||
- **Cross-Platform**: Optimized for x86_64, aarch64, s390x, and other architectures
|
||||
|
||||
### 🎫 JWT Management
|
||||
|
||||
- **Token Generation**: Secure JWT token creation with HS512 algorithm
|
||||
- **Token Validation**: Robust JWT token verification and decoding
|
||||
- **Claims Management**: Flexible claims handling with JSON support
|
||||
|
||||
### 🛡️ Security Features
|
||||
|
||||
- **FIPS Compliance**: Optional FIPS 140-2 compatible mode
|
||||
- **Hardware Acceleration**: Automatic detection and utilization of CPU crypto extensions
|
||||
- **Secure Random**: Cryptographically secure random number generation
|
||||
- **Side-Channel Protection**: Resistant to timing attacks
|
||||
|
||||
### 🚀 Performance
|
||||
|
||||
- **Zero-Copy Operations**: Efficient memory usage with `Bytes` support
|
||||
- **Async/Await**: Full async support for non-blocking operations
|
||||
- **Hardware Optimization**: CPU-specific optimizations for better performance
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-crypto = "0.1.0"
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-crypto = { version = "0.1.0", features = ["crypto", "fips"] }
|
||||
```
|
||||
|
||||
Available features:
|
||||
|
||||
- `crypto` (default): Enable all cryptographic functions
|
||||
- `fips`: Enable FIPS 140-2 compliance mode
|
||||
- `default`: Includes both `crypto` and `fips`
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Encryption/Decryption
|
||||
|
||||
```rust
|
||||
use rustfs_crypto::{encrypt_data, decrypt_data};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let password = b"my_secure_password";
|
||||
let data = b"sensitive information";
|
||||
|
||||
// Encrypt data
|
||||
let encrypted = encrypt_data(password, data)?;
|
||||
println!("Encrypted {} bytes", encrypted.len());
|
||||
|
||||
// Decrypt data
|
||||
let decrypted = decrypt_data(password, &encrypted)?;
|
||||
assert_eq!(data, decrypted.as_slice());
|
||||
println!("Successfully decrypted data");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### JWT Token Management
|
||||
|
||||
```rust
|
||||
use rustfs_crypto::{jwt_encode, jwt_decode};
|
||||
use serde_json::json;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let secret = b"jwt_secret_key";
|
||||
let claims = json!({
|
||||
"sub": "user123",
|
||||
"exp": 1234567890,
|
||||
"iat": 1234567890
|
||||
});
|
||||
|
||||
// Create JWT token
|
||||
let token = jwt_encode(secret, &claims)?;
|
||||
println!("Generated token: {}", token);
|
||||
|
||||
// Verify and decode token
|
||||
let decoded = jwt_decode(&token, secret)?;
|
||||
println!("Decoded claims: {:?}", decoded.claims);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Usage with Custom Configuration
|
||||
|
||||
```rust
|
||||
use rustfs_crypto::{encrypt_data, decrypt_data, Error};
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
fn secure_storage_example() -> Result<(), Error> {
|
||||
// Large data encryption
|
||||
let large_data = vec![0u8; 1024 * 1024]; // 1MB
|
||||
let password = b"complex_password_123!@#";
|
||||
|
||||
// Encrypt with automatic algorithm selection
|
||||
let encrypted = encrypt_data(password, &large_data)?;
|
||||
|
||||
// Decrypt and verify
|
||||
let decrypted = decrypt_data(password, &encrypted)?;
|
||||
assert_eq!(large_data.len(), decrypted.len());
|
||||
|
||||
println!("Successfully processed {} bytes", large_data.len());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Supported Encryption Algorithms
|
||||
|
||||
| Algorithm | Key Derivation | Use Case | FIPS Compliant |
|
||||
|-----------|---------------|----------|----------------|
|
||||
| AES-GCM | Argon2id | General purpose, hardware accelerated | ✅ |
|
||||
| ChaCha20Poly1305 | Argon2id | Software-only environments | ❌ |
|
||||
| AES-GCM | PBKDF2 | FIPS compliance required | ✅ |
|
||||
|
||||
### Cross-Platform Support
|
||||
|
||||
The module automatically detects and optimizes for:
|
||||
|
||||
- **x86/x86_64**: AES-NI and PCLMULQDQ instructions
|
||||
- **aarch64**: ARM Crypto Extensions
|
||||
- **s390x**: IBM Z Crypto Extensions
|
||||
- **Other architectures**: Fallback to software implementations
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run tests with all features
|
||||
cargo test --all-features
|
||||
|
||||
# Run benchmarks
|
||||
cargo bench
|
||||
|
||||
# Test cross-platform compatibility
|
||||
cargo test --target x86_64-unknown-linux-gnu
|
||||
cargo test --target aarch64-unknown-linux-gnu
|
||||
```
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
The crypto module is designed for high-performance scenarios:
|
||||
|
||||
- **Encryption Speed**: Up to 2GB/s on modern hardware
|
||||
- **Memory Usage**: Minimal heap allocation with zero-copy operations
|
||||
- **CPU Utilization**: Automatic hardware acceleration detection
|
||||
- **Scalability**: Thread-safe operations for concurrent access
|
||||
|
||||
## 🤝 Integration with RustFS
|
||||
|
||||
This module is specifically designed to integrate with other RustFS components:
|
||||
|
||||
- **Storage Layer**: Provides encryption for object storage
|
||||
- **Authentication**: JWT tokens for API authentication
|
||||
- **Configuration**: Secure configuration data encryption
|
||||
- **Metadata**: Encrypted metadata storage
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Architectures**: x86_64, aarch64, s390x, and more
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
- All cryptographic operations use industry-standard algorithms
|
||||
- Key derivation follows best practices (Argon2id, PBKDF2)
|
||||
- Memory is securely cleared after use
|
||||
- Timing attack resistance is built-in
|
||||
- Hardware security modules (HSM) support planned
|
||||
|
||||
## 🐛 Known Issues
|
||||
|
||||
- Hardware acceleration detection may not work on all virtualized environments
|
||||
- FIPS mode requires additional system-level configuration
|
||||
- Some older CPU architectures may have reduced performance
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS ECStore](../ecstore) - Erasure coding storage engine
|
||||
- [RustFS IAM](../iam) - Identity and access management
|
||||
- [RustFS Policy](../policy) - Policy engine
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [API Reference](https://docs.rustfs.com/crypto/)
|
||||
- [Security Guide](https://docs.rustfs.com/security/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details on:
|
||||
|
||||
- Code style and formatting requirements
|
||||
- Testing procedures and coverage
|
||||
- Security considerations for cryptographic code
|
||||
- Pull request process and review guidelines
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/rustfs/rustfs.git
|
||||
cd rustfs
|
||||
|
||||
# Navigate to crypto module
|
||||
cd crates/crypto
|
||||
|
||||
# Install dependencies
|
||||
cargo build
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Format code
|
||||
cargo fmt
|
||||
|
||||
# Run linter
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
## 💬 Getting Help
|
||||
|
||||
- **Documentation**: [docs.rustfs.com](https://docs.rustfs.com)
|
||||
- **Issues**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
- **Security**: Report security issues to <security@rustfs.com>
|
||||
|
||||
## 📞 Contact
|
||||
|
||||
- **Bugs**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||
- **Business**: <hello@rustfs.com>
|
||||
- **Jobs**: <jobs@rustfs.com>
|
||||
- **General Discussion**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
This module is maintained by the RustFS team and community contributors. Special thanks to all who have contributed to making RustFS cryptography secure and efficient.
|
||||
|
||||
<a href="https://github.com/rustfs/rustfs/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=rustfs/rustfs" />
|
||||
</a>
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,465 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS ECStore - Erasure Coding Storage Engine
|
||||
|
||||
<p align="center">
|
||||
<strong>High-performance erasure coding storage engine for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS ECStore** is the core storage engine of the [RustFS](https://rustfs.com) distributed object storage system. It provides enterprise-grade erasure coding capabilities, data integrity protection, and high-performance object storage operations. This module serves as the foundation for RustFS's distributed storage architecture.
|
||||
|
||||
> **Note:** This is a core submodule of RustFS and provides the primary storage capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔧 Erasure Coding Storage
|
||||
|
||||
- **Reed-Solomon Erasure Coding**: Advanced error correction with configurable redundancy
|
||||
- **Data Durability**: Protection against disk failures and bit rot
|
||||
- **Automatic Repair**: Self-healing capabilities for corrupted or missing data
|
||||
- **Configurable Parity**: Flexible parity configurations (4+2, 8+4, 16+4, etc.)
|
||||
|
||||
### 💾 Storage Management
|
||||
|
||||
- **Multi-Disk Support**: Intelligent disk management and load balancing
|
||||
- **Storage Classes**: Support for different storage tiers and policies
|
||||
- **Bucket Management**: Advanced bucket operations and lifecycle management
|
||||
- **Object Versioning**: Complete versioning support with metadata tracking
|
||||
|
||||
### 🚀 Performance & Scalability
|
||||
|
||||
- **High Throughput**: Optimized for large-scale data operations
|
||||
- **Parallel Processing**: Concurrent read/write operations across multiple disks
|
||||
- **Memory Efficient**: Smart caching and memory management
|
||||
- **SIMD Optimization**: Hardware-accelerated erasure coding operations
|
||||
|
||||
### 🛡️ Data Integrity
|
||||
|
||||
- **Bitrot Detection**: Real-time data corruption detection
|
||||
- **Checksum Verification**: Multiple checksum algorithms (MD5, SHA256, XXHash)
|
||||
- **Healing System**: Automatic background healing and repair
|
||||
- **Data Scrubbing**: Proactive data integrity scanning
|
||||
|
||||
### 🔄 Advanced Features
|
||||
|
||||
- **Compression**: Built-in compression support for space optimization
|
||||
- **Replication**: Cross-region replication capabilities
|
||||
- **Notification System**: Real-time event notifications
|
||||
- **Metrics & Monitoring**: Comprehensive performance metrics
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Storage Layout
|
||||
|
||||
```
|
||||
ECStore Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Storage API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Bucket Management │ Object Operations │ Metadata Mgmt │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Erasure Coding Engine │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Disk Management │ Healing System │ Cache │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Physical Storage Devices │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Erasure Coding Schemes
|
||||
|
||||
| Configuration | Data Drives | Parity Drives | Fault Tolerance | Storage Efficiency |
|
||||
|---------------|-------------|---------------|-----------------|-------------------|
|
||||
| 4+2 | 4 | 2 | 2 disk failures | 66.7% |
|
||||
| 8+4 | 8 | 4 | 4 disk failures | 66.7% |
|
||||
| 16+4 | 16 | 4 | 4 disk failures | 80% |
|
||||
| Custom | N | K | K disk failures | N/(N+K) |
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-ecstore = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Storage Operations
|
||||
|
||||
```rust
|
||||
use rustfs_ecstore::{StorageAPI, new_object_layer_fn};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize storage layer
|
||||
let storage = new_object_layer_fn("/path/to/storage").await?;
|
||||
|
||||
// Create a bucket
|
||||
storage.make_bucket("my-bucket", None).await?;
|
||||
|
||||
// Put an object
|
||||
let data = b"Hello, RustFS!";
|
||||
storage.put_object("my-bucket", "hello.txt", data.to_vec()).await?;
|
||||
|
||||
// Get an object
|
||||
let retrieved = storage.get_object("my-bucket", "hello.txt", None).await?;
|
||||
println!("Retrieved: {}", String::from_utf8_lossy(&retrieved.data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```rust
|
||||
use rustfs_ecstore::{StorageAPI, config::Config};
|
||||
|
||||
async fn setup_storage_with_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config {
|
||||
erasure_sets: vec![
|
||||
// 8+4 configuration for high durability
|
||||
ErasureSet::new(8, 4, vec![
|
||||
"/disk1", "/disk2", "/disk3", "/disk4",
|
||||
"/disk5", "/disk6", "/disk7", "/disk8",
|
||||
"/disk9", "/disk10", "/disk11", "/disk12"
|
||||
])
|
||||
],
|
||||
healing_enabled: true,
|
||||
compression_enabled: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let storage = new_object_layer_fn("/path/to/storage")
|
||||
.with_config(config)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Bucket Management
|
||||
|
||||
```rust
|
||||
use rustfs_ecstore::{StorageAPI, bucket::BucketInfo};
|
||||
|
||||
async fn bucket_operations(storage: Arc<dyn StorageAPI>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create bucket with specific configuration
|
||||
let bucket_info = BucketInfo {
|
||||
name: "enterprise-bucket".to_string(),
|
||||
versioning_enabled: true,
|
||||
lifecycle_config: Some(lifecycle_config()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
storage.make_bucket_with_config(bucket_info).await?;
|
||||
|
||||
// List buckets
|
||||
let buckets = storage.list_buckets().await?;
|
||||
for bucket in buckets {
|
||||
println!("Bucket: {}, Created: {}", bucket.name, bucket.created);
|
||||
}
|
||||
|
||||
// Set bucket policy
|
||||
storage.set_bucket_policy("enterprise-bucket", policy_json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Healing and Maintenance
|
||||
|
||||
```rust
|
||||
use rustfs_ecstore::{heal::HealingManager, StorageAPI};
|
||||
|
||||
async fn healing_operations(storage: Arc<dyn StorageAPI>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Check storage health
|
||||
let health = storage.storage_info().await?;
|
||||
println!("Storage Health: {:?}", health);
|
||||
|
||||
// Trigger healing for specific bucket
|
||||
let healing_result = storage.heal_bucket("my-bucket").await?;
|
||||
println!("Healing completed: {:?}", healing_result);
|
||||
|
||||
// Background healing status
|
||||
let healing_status = storage.healing_status().await?;
|
||||
println!("Background healing: {:?}", healing_status);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run tests with specific features
|
||||
cargo test --features "compression,healing"
|
||||
|
||||
# Run benchmarks
|
||||
cargo bench
|
||||
|
||||
# Run erasure coding benchmarks
|
||||
cargo bench --bench erasure_benchmark
|
||||
|
||||
# Run comparison benchmarks
|
||||
cargo bench --bench comparison_benchmark
|
||||
```
|
||||
|
||||
## 📊 Performance Benchmarks
|
||||
|
||||
ECStore is designed for high-performance storage operations:
|
||||
|
||||
### Throughput Performance
|
||||
|
||||
- **Sequential Write**: Up to 10GB/s on NVMe storage
|
||||
- **Sequential Read**: Up to 12GB/s with parallel reads
|
||||
- **Random I/O**: 100K+ IOPS for small objects
|
||||
- **Erasure Coding**: 5GB/s encoding/decoding throughput
|
||||
|
||||
### Scalability Metrics
|
||||
|
||||
- **Storage Capacity**: Exabyte-scale deployments
|
||||
- **Concurrent Operations**: 10,000+ concurrent requests
|
||||
- **Disk Scaling**: Support for 1000+ disks per node
|
||||
- **Fault Tolerance**: Up to 50% disk failure resilience
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Storage Configuration
|
||||
|
||||
```toml
|
||||
[storage]
|
||||
# Erasure coding configuration
|
||||
erasure_set_size = 12 # Total disks per set
|
||||
data_drives = 8 # Data drives per set
|
||||
parity_drives = 4 # Parity drives per set
|
||||
|
||||
# Performance tuning
|
||||
read_quorum = 6 # Minimum disks for read
|
||||
write_quorum = 7 # Minimum disks for write
|
||||
parallel_reads = true # Enable parallel reads
|
||||
compression = true # Enable compression
|
||||
|
||||
# Healing configuration
|
||||
healing_enabled = true
|
||||
healing_interval = "24h"
|
||||
bitrot_check_interval = "168h" # Weekly bitrot check
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
```rust
|
||||
use rustfs_ecstore::config::StorageConfig;
|
||||
|
||||
let config = StorageConfig {
|
||||
// Enable advanced features
|
||||
bitrot_protection: true,
|
||||
automatic_healing: true,
|
||||
compression_level: 6,
|
||||
checksum_algorithm: ChecksumAlgorithm::XXHash64,
|
||||
|
||||
// Performance tuning
|
||||
read_buffer_size: 1024 * 1024, // 1MB read buffer
|
||||
write_buffer_size: 4 * 1024 * 1024, // 4MB write buffer
|
||||
concurrent_operations: 1000,
|
||||
|
||||
// Storage optimization
|
||||
small_object_threshold: 128 * 1024, // 128KB
|
||||
large_object_threshold: 64 * 1024 * 1024, // 64MB
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
```
|
||||
|
||||
## 🤝 Integration with RustFS
|
||||
|
||||
ECStore integrates seamlessly with other RustFS components:
|
||||
|
||||
- **API Server**: Provides S3-compatible storage operations
|
||||
- **IAM Module**: Handles authentication and authorization
|
||||
- **Policy Engine**: Implements bucket policies and access controls
|
||||
- **Notification System**: Publishes storage events
|
||||
- **Monitoring**: Provides detailed metrics and health status
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Storage**: Local disks, network storage, cloud storage
|
||||
- **Memory**: Minimum 4GB RAM (8GB+ recommended)
|
||||
- **Network**: High-speed network for distributed deployments
|
||||
|
||||
## 🚀 Performance Tuning
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Disk Configuration**:
|
||||
- Use dedicated disks for each erasure set
|
||||
- Prefer NVMe over SATA for better performance
|
||||
- Ensure consistent disk sizes within erasure sets
|
||||
|
||||
2. **Memory Settings**:
|
||||
- Allocate sufficient memory for caching
|
||||
- Tune read/write buffer sizes based on workload
|
||||
- Enable memory-mapped files for large objects
|
||||
|
||||
3. **Network Optimization**:
|
||||
- Use high-speed network connections
|
||||
- Configure proper MTU sizes
|
||||
- Enable network compression for WAN scenarios
|
||||
|
||||
4. **CPU Optimization**:
|
||||
- Utilize SIMD instructions for erasure coding
|
||||
- Balance CPU cores across erasure sets
|
||||
- Enable hardware-accelerated checksums
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Disk Failures**:
|
||||
- Check disk health using `storage_info()`
|
||||
- Trigger healing with `heal_bucket()`
|
||||
- Replace failed disks and re-add to cluster
|
||||
|
||||
2. **Performance Issues**:
|
||||
- Monitor disk I/O utilization
|
||||
- Check network bandwidth usage
|
||||
- Verify erasure coding configuration
|
||||
|
||||
3. **Data Integrity**:
|
||||
- Run bitrot detection scans
|
||||
- Verify checksums for critical data
|
||||
- Check healing system status
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Crypto](../crypto) - Cryptographic operations
|
||||
- [RustFS IAM](../iam) - Identity and access management
|
||||
- [RustFS Policy](../policy) - Policy engine
|
||||
- [RustFS FileMeta](../filemeta) - File metadata management
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Storage API Reference](https://docs.rustfs.com/ecstore/)
|
||||
- [Erasure Coding Guide](https://docs.rustfs.com/erasure-coding/)
|
||||
- [Performance Tuning](https://docs.rustfs.com/performance/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details on:
|
||||
|
||||
- Storage engine architecture and design patterns
|
||||
- Erasure coding implementation guidelines
|
||||
- Performance optimization techniques
|
||||
- Testing procedures for storage operations
|
||||
- Documentation standards for storage APIs
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/rustfs/rustfs.git
|
||||
cd rustfs
|
||||
|
||||
# Navigate to ECStore module
|
||||
cd crates/ecstore
|
||||
|
||||
# Install dependencies
|
||||
cargo build
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Run benchmarks
|
||||
cargo bench
|
||||
|
||||
# Format code
|
||||
cargo fmt
|
||||
|
||||
# Run linter
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
## 💬 Getting Help
|
||||
|
||||
- **Documentation**: [docs.rustfs.com](https://docs.rustfs.com)
|
||||
- **Issues**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
- **Storage Support**: <storage-support@rustfs.com>
|
||||
|
||||
## 📞 Contact
|
||||
|
||||
- **Bugs**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||
- **Business**: <hello@rustfs.com>
|
||||
- **Jobs**: <jobs@rustfs.com>
|
||||
- **General Discussion**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
This module is maintained by the RustFS storage team and community contributors. Special thanks to all who have contributed to making RustFS storage reliable and efficient.
|
||||
|
||||
<a href="https://github.com/rustfs/rustfs/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=rustfs/rustfs" />
|
||||
</a>
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ by the RustFS Storage Team
|
||||
</p>
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(unused_imports)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,6 +11,7 @@
|
||||
// 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.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_assignments)]
|
||||
@@ -41,7 +41,7 @@ const ERR_LIFECYCLE_DUPLICATE_ID: &str = "Rule ID must be unique. Found same ID
|
||||
const _ERR_XML_NOT_WELL_FORMED: &str =
|
||||
"The XML you provided was not well-formed or did not validate against our published schema";
|
||||
const ERR_LIFECYCLE_BUCKET_LOCKED: &str =
|
||||
"ExpiredObjectAllVersions element and DelMarkerExpiration action cannot be used on an object locked bucket";
|
||||
"ExpiredObjectAllVersions element and DelMarkerExpiration action cannot be used on an retention bucket";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IlmAction {
|
||||
@@ -102,30 +102,30 @@ impl RuleValidate for LifecycleRule {
|
||||
}
|
||||
|
||||
fn validate_status(&self) -> Result<()> {
|
||||
if self.Status.len() == 0 {
|
||||
return errEmptyRuleStatus;
|
||||
if self.status.len() == 0 {
|
||||
return ErrEmptyRuleStatus;
|
||||
}
|
||||
|
||||
if self.Status != Enabled && self.Status != Disabled {
|
||||
return errInvalidRuleStatus;
|
||||
if self.status != Enabled && self.status != Disabled {
|
||||
return ErrInvalidRuleStatus;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_expiration(&self) -> Result<()> {
|
||||
self.Expiration.Validate();
|
||||
self.expiration.validate();
|
||||
}
|
||||
|
||||
fn validate_noncurrent_expiration(&self) -> Result<()> {
|
||||
self.NoncurrentVersionExpiration.Validate()
|
||||
self.noncurrent_version_expiration.validate()
|
||||
}
|
||||
|
||||
fn validate_prefix_and_filter(&self) -> Result<()> {
|
||||
if !self.Prefix.set && self.Filter.IsEmpty() || self.Prefix.set && !self.Filter.IsEmpty() {
|
||||
return errXMLNotWellFormed;
|
||||
if !self.prefix.set && self.Filter.isempty() || self.prefix.set && !self.filter.isempty() {
|
||||
return ErrXMLNotWellFormed;
|
||||
}
|
||||
if !self.Prefix.set {
|
||||
return self.Filter.Validate();
|
||||
if !self.prefix.set {
|
||||
return self.filter.validate();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -267,7 +267,7 @@ impl Lifecycle for BucketLifecycleConfiguration {
|
||||
r.validate()?;
|
||||
if let Some(expiration) = r.expiration.as_ref() {
|
||||
if let Some(expired_object_delete_marker) = expiration.expired_object_delete_marker {
|
||||
if lr_retention && (!expired_object_delete_marker) {
|
||||
if lr_retention && (expired_object_delete_marker) {
|
||||
return Err(std::io::Error::other(ERR_LIFECYCLE_BUCKET_LOCKED));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::client::{api_put_object::PutObjectOptions, api_s3_datatypes::ObjectPart};
|
||||
use crate::{disk::DiskAPI, store_api::GetObjectReader};
|
||||
use rustfs_utils::crypto::{base64_decode, base64_encode};
|
||||
use rustfs_utils::hasher::{Hasher, Sha256};
|
||||
use s3s::header::{
|
||||
X_AMZ_CHECKSUM_ALGORITHM, X_AMZ_CHECKSUM_CRC32, X_AMZ_CHECKSUM_CRC32C, X_AMZ_CHECKSUM_SHA1, X_AMZ_CHECKSUM_SHA256,
|
||||
};
|
||||
@@ -133,7 +133,7 @@ impl ChecksumMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hasher(&self) -> Result<Box<dyn Hasher>, std::io::Error> {
|
||||
pub fn hasher(&self) -> Result<HashAlgorithm, std::io::Error> {
|
||||
match /*C_ChecksumMask & **/self {
|
||||
/*ChecksumMode::ChecksumCRC32 => {
|
||||
return Ok(Box::new(crc32fast::Hasher::new()));
|
||||
@@ -145,7 +145,7 @@ impl ChecksumMode {
|
||||
return Ok(Box::new(sha1::new()));
|
||||
}*/
|
||||
ChecksumMode::ChecksumSHA256 => {
|
||||
return Ok(Box::new(Sha256::new()));
|
||||
return Ok(HashAlgorithm::SHA256);
|
||||
}
|
||||
/*ChecksumMode::ChecksumCRC64NVME => {
|
||||
return Ok(Box::new(crc64nvme.New());
|
||||
@@ -170,8 +170,8 @@ impl ChecksumMode {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
let mut h = self.hasher()?;
|
||||
h.write(b);
|
||||
Ok(base64_encode(h.sum().as_bytes()))
|
||||
let hash = h.hash_encode(b);
|
||||
Ok(base64_encode(hash.as_ref()))
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
@@ -201,15 +201,15 @@ impl ChecksumMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_sum_reader(&self, r: GetObjectReader) -> Result<Checksum, std::io::Error> {
|
||||
let mut h = self.hasher()?;
|
||||
Ok(Checksum::new(self.clone(), h.sum().as_bytes()))
|
||||
}
|
||||
// pub fn check_sum_reader(&self, r: GetObjectReader) -> Result<Checksum, std::io::Error> {
|
||||
// let mut h = self.hasher()?;
|
||||
// Ok(Checksum::new(self.clone(), h.sum().as_bytes()))
|
||||
// }
|
||||
|
||||
pub fn check_sum_bytes(&self, b: &[u8]) -> Result<Checksum, std::io::Error> {
|
||||
let mut h = self.hasher()?;
|
||||
Ok(Checksum::new(self.clone(), h.sum().as_bytes()))
|
||||
}
|
||||
// pub fn check_sum_bytes(&self, b: &[u8]) -> Result<Checksum, std::io::Error> {
|
||||
// let mut h = self.hasher()?;
|
||||
// Ok(Checksum::new(self.clone(), h.sum().as_bytes()))
|
||||
// }
|
||||
|
||||
pub fn composite_checksum(&self, p: &mut [ObjectPart]) -> Result<Checksum, std::io::Error> {
|
||||
if !self.can_composite() {
|
||||
@@ -227,10 +227,10 @@ impl ChecksumMode {
|
||||
let c = self.base();
|
||||
let crc_bytes = Vec::<u8>::with_capacity(p.len() * self.raw_byte_len() as usize);
|
||||
let mut h = self.hasher()?;
|
||||
h.write(&crc_bytes);
|
||||
let hash = h.hash_encode(crc_bytes.as_ref());
|
||||
Ok(Checksum {
|
||||
checksum_type: self.clone(),
|
||||
r: h.sum().as_bytes().to_vec(),
|
||||
r: hash.as_ref().to_vec(),
|
||||
computed: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// 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.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_assignments)]
|
||||
#![allow(unused_must_use)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use s3s::dto::Owner;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::BufReader;
|
||||
|
||||
use crate::client::{
|
||||
api_error_response::{err_invalid_argument, http_resp_to_error_response},
|
||||
api_get_options::GetObjectOptions,
|
||||
transition_api::{ObjectInfo, ReadCloser, ReaderImpl, RequestMetadata, TransitionClient, to_object_info},
|
||||
};
|
||||
use rustfs_utils::EMPTY_STRING_SHA256_HASH;
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Grantee {
|
||||
pub id: String,
|
||||
pub display_name: String,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Grant {
|
||||
pub grantee: Grantee,
|
||||
pub permission: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AccessControlList {
|
||||
pub grant: Vec<Grant>,
|
||||
pub permission: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
pub struct AccessControlPolicy {
|
||||
#[serde(skip)]
|
||||
owner: Owner,
|
||||
pub access_control_list: AccessControlList,
|
||||
}
|
||||
|
||||
impl TransitionClient {
|
||||
pub async fn get_object_acl(&self, bucket_name: &str, object_name: &str) -> Result<ObjectInfo, std::io::Error> {
|
||||
let mut url_values = HashMap::new();
|
||||
url_values.insert("acl".to_string(), "".to_string());
|
||||
let mut resp = self
|
||||
.execute_method(
|
||||
http::Method::GET,
|
||||
&mut RequestMetadata {
|
||||
bucket_name: bucket_name.to_string(),
|
||||
object_name: object_name.to_string(),
|
||||
query_values: url_values,
|
||||
custom_header: HeaderMap::new(),
|
||||
content_sha256_hex: EMPTY_STRING_SHA256_HASH.to_string(),
|
||||
content_body: ReaderImpl::Body(Bytes::new()),
|
||||
content_length: 0,
|
||||
content_md5_base64: "".to_string(),
|
||||
stream_sha256: false,
|
||||
trailer: HeaderMap::new(),
|
||||
pre_sign_url: Default::default(),
|
||||
add_crc: Default::default(),
|
||||
extra_pre_sign_header: Default::default(),
|
||||
bucket_location: Default::default(),
|
||||
expires: Default::default(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if resp.status() != http::StatusCode::OK {
|
||||
let b = resp.body().bytes().expect("err").to_vec();
|
||||
return Err(std::io::Error::other(http_resp_to_error_response(resp, b, bucket_name, object_name)));
|
||||
}
|
||||
|
||||
let b = resp.body_mut().store_all_unlimited().await.unwrap().to_vec();
|
||||
let mut res = match serde_xml_rs::from_str::<AccessControlPolicy>(&String::from_utf8(b).unwrap()) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let mut obj_info = self
|
||||
.stat_object(bucket_name, object_name, &GetObjectOptions::default())
|
||||
.await?;
|
||||
|
||||
obj_info.owner.display_name = res.owner.display_name.clone();
|
||||
obj_info.owner.id = res.owner.id.clone();
|
||||
|
||||
//obj_info.grant.extend(res.access_control_list.grant);
|
||||
|
||||
let canned_acl = get_canned_acl(&res);
|
||||
if canned_acl != "" {
|
||||
obj_info
|
||||
.metadata
|
||||
.insert("X-Amz-Acl", HeaderValue::from_str(&canned_acl).unwrap());
|
||||
return Ok(obj_info);
|
||||
}
|
||||
|
||||
let grant_acl = get_amz_grant_acl(&res);
|
||||
/*for (k, v) in grant_acl {
|
||||
obj_info.metadata.insert(HeaderName::from_bytes(k.as_bytes()).unwrap(), HeaderValue::from_str(&v.to_string()).unwrap());
|
||||
}*/
|
||||
|
||||
Ok(obj_info)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_canned_acl(ac_policy: &AccessControlPolicy) -> String {
|
||||
let grants = ac_policy.access_control_list.grant.clone();
|
||||
|
||||
if grants.len() == 1 {
|
||||
if grants[0].grantee.uri == "" && grants[0].permission == "FULL_CONTROL" {
|
||||
return "private".to_string();
|
||||
}
|
||||
} else if grants.len() == 2 {
|
||||
for g in grants {
|
||||
if g.grantee.uri == "http://acs.amazonaws.com/groups/global/AuthenticatedUsers" && &g.permission == "READ" {
|
||||
return "authenticated-read".to_string();
|
||||
}
|
||||
if g.grantee.uri == "http://acs.amazonaws.com/groups/global/AllUsers" && &g.permission == "READ" {
|
||||
return "public-read".to_string();
|
||||
}
|
||||
if g.permission == "READ" && g.grantee.id == ac_policy.owner.id.clone().unwrap() {
|
||||
return "bucket-owner-read".to_string();
|
||||
}
|
||||
}
|
||||
} else if grants.len() == 3 {
|
||||
for g in grants {
|
||||
if g.grantee.uri == "http://acs.amazonaws.com/groups/global/AllUsers" && g.permission == "WRITE" {
|
||||
return "public-read-write".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
pub fn get_amz_grant_acl(ac_policy: &AccessControlPolicy) -> HashMap<String, Vec<String>> {
|
||||
let grants = ac_policy.access_control_list.grant.clone();
|
||||
let mut res = HashMap::<String, Vec<String>>::new();
|
||||
|
||||
for g in grants {
|
||||
let mut id = "id=".to_string();
|
||||
id.push_str(&g.grantee.id);
|
||||
let permission: &str = &g.permission;
|
||||
match permission {
|
||||
"READ" => {
|
||||
res.entry("X-Amz-Grant-Read".to_string()).or_insert(vec![]).push(id);
|
||||
}
|
||||
"WRITE" => {
|
||||
res.entry("X-Amz-Grant-Write".to_string()).or_insert(vec![]).push(id);
|
||||
}
|
||||
"READ_ACP" => {
|
||||
res.entry("X-Amz-Grant-Read-Acp".to_string()).or_insert(vec![]).push(id);
|
||||
}
|
||||
"WRITE_ACP" => {
|
||||
res.entry("X-Amz-Grant-Write-Acp".to_string()).or_insert(vec![]).push(id);
|
||||
}
|
||||
"FULL_CONTROL" => {
|
||||
res.entry("X-Amz-Grant-Full-Control".to_string()).or_insert(vec![]).push(id);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// 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.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_assignments)]
|
||||
#![allow(unused_must_use)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::io::BufReader;
|
||||
|
||||
use crate::client::constants::{GET_OBJECT_ATTRIBUTES_MAX_PARTS, GET_OBJECT_ATTRIBUTES_TAGS, ISO8601_DATEFORMAT};
|
||||
use rustfs_utils::EMPTY_STRING_SHA256_HASH;
|
||||
use s3s::header::{
|
||||
X_AMZ_DELETE_MARKER, X_AMZ_MAX_PARTS, X_AMZ_METADATA_DIRECTIVE, X_AMZ_OBJECT_ATTRIBUTES, X_AMZ_PART_NUMBER_MARKER,
|
||||
X_AMZ_REQUEST_CHARGED, X_AMZ_RESTORE, X_AMZ_VERSION_ID,
|
||||
};
|
||||
use s3s::{Body, dto::Owner};
|
||||
|
||||
use crate::client::{
|
||||
api_error_response::err_invalid_argument,
|
||||
api_get_object_acl::AccessControlPolicy,
|
||||
api_get_options::GetObjectOptions,
|
||||
transition_api::{ObjectInfo, ReadCloser, ReaderImpl, RequestMetadata, TransitionClient, to_object_info},
|
||||
};
|
||||
|
||||
pub struct ObjectAttributesOptions {
|
||||
pub max_parts: i64,
|
||||
pub version_id: String,
|
||||
pub part_number_marker: i64,
|
||||
//server_side_encryption: encrypt::ServerSide,
|
||||
}
|
||||
|
||||
pub struct ObjectAttributes {
|
||||
pub version_id: String,
|
||||
pub last_modified: OffsetDateTime,
|
||||
pub object_attributes_response: ObjectAttributesResponse,
|
||||
}
|
||||
|
||||
impl ObjectAttributes {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
version_id: "".to_string(),
|
||||
last_modified: OffsetDateTime::now_utc(),
|
||||
object_attributes_response: ObjectAttributesResponse::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
pub struct Checksum {
|
||||
checksum_crc32: String,
|
||||
checksum_crc32c: String,
|
||||
checksum_sha1: String,
|
||||
checksum_sha256: String,
|
||||
}
|
||||
|
||||
impl Checksum {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
checksum_crc32: "".to_string(),
|
||||
checksum_crc32c: "".to_string(),
|
||||
checksum_sha1: "".to_string(),
|
||||
checksum_sha256: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
pub struct ObjectParts {
|
||||
pub parts_count: i64,
|
||||
pub part_number_marker: i64,
|
||||
pub next_part_number_marker: i64,
|
||||
pub max_parts: i64,
|
||||
is_truncated: bool,
|
||||
parts: Vec<ObjectAttributePart>,
|
||||
}
|
||||
|
||||
impl ObjectParts {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
parts_count: 0,
|
||||
part_number_marker: 0,
|
||||
next_part_number_marker: 0,
|
||||
max_parts: 0,
|
||||
is_truncated: false,
|
||||
parts: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
pub struct ObjectAttributesResponse {
|
||||
pub etag: String,
|
||||
pub storage_class: String,
|
||||
pub object_size: i64,
|
||||
pub checksum: Checksum,
|
||||
pub object_parts: ObjectParts,
|
||||
}
|
||||
|
||||
impl ObjectAttributesResponse {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
etag: "".to_string(),
|
||||
storage_class: "".to_string(),
|
||||
object_size: 0,
|
||||
checksum: Checksum::new(),
|
||||
object_parts: ObjectParts::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
struct ObjectAttributePart {
|
||||
checksum_crc32: String,
|
||||
checksum_crc32c: String,
|
||||
checksum_sha1: String,
|
||||
checksum_sha256: String,
|
||||
part_number: i64,
|
||||
size: i64,
|
||||
}
|
||||
|
||||
impl ObjectAttributes {
|
||||
pub async fn parse_response(&mut self, resp: &mut http::Response<Body>) -> Result<(), std::io::Error> {
|
||||
let h = resp.headers();
|
||||
let mod_time = OffsetDateTime::parse(h.get("Last-Modified").unwrap().to_str().unwrap(), ISO8601_DATEFORMAT).unwrap(); //RFC7231Time
|
||||
self.last_modified = mod_time;
|
||||
self.version_id = h.get(X_AMZ_VERSION_ID).unwrap().to_str().unwrap().to_string();
|
||||
|
||||
let b = resp.body_mut().store_all_unlimited().await.unwrap().to_vec();
|
||||
let mut response = match serde_xml_rs::from_str::<ObjectAttributesResponse>(&String::from_utf8(b).unwrap()) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err.to_string()));
|
||||
}
|
||||
};
|
||||
self.object_attributes_response = response;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TransitionClient {
|
||||
pub async fn get_object_attributes(
|
||||
&self,
|
||||
bucket_name: &str,
|
||||
object_name: &str,
|
||||
opts: ObjectAttributesOptions,
|
||||
) -> Result<ObjectAttributes, std::io::Error> {
|
||||
let mut url_values = HashMap::new();
|
||||
url_values.insert("attributes".to_string(), "".to_string());
|
||||
if opts.version_id != "" {
|
||||
url_values.insert("versionId".to_string(), opts.version_id);
|
||||
}
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(X_AMZ_OBJECT_ATTRIBUTES, HeaderValue::from_str(GET_OBJECT_ATTRIBUTES_TAGS).unwrap());
|
||||
|
||||
if opts.part_number_marker > 0 {
|
||||
headers.insert(
|
||||
X_AMZ_PART_NUMBER_MARKER,
|
||||
HeaderValue::from_str(&opts.part_number_marker.to_string()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if opts.max_parts > 0 {
|
||||
headers.insert(X_AMZ_MAX_PARTS, HeaderValue::from_str(&opts.max_parts.to_string()).unwrap());
|
||||
} else {
|
||||
headers.insert(
|
||||
X_AMZ_MAX_PARTS,
|
||||
HeaderValue::from_str(&GET_OBJECT_ATTRIBUTES_MAX_PARTS.to_string()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
/*if opts.server_side_encryption.is_some() {
|
||||
opts.server_side_encryption.Marshal(headers);
|
||||
}*/
|
||||
|
||||
let mut resp = self
|
||||
.execute_method(
|
||||
http::Method::HEAD,
|
||||
&mut RequestMetadata {
|
||||
bucket_name: bucket_name.to_string(),
|
||||
object_name: object_name.to_string(),
|
||||
query_values: url_values,
|
||||
custom_header: headers,
|
||||
content_sha256_hex: EMPTY_STRING_SHA256_HASH.to_string(),
|
||||
content_md5_base64: "".to_string(),
|
||||
content_body: ReaderImpl::Body(Bytes::new()),
|
||||
content_length: 0,
|
||||
stream_sha256: false,
|
||||
trailer: HeaderMap::new(),
|
||||
pre_sign_url: Default::default(),
|
||||
add_crc: Default::default(),
|
||||
extra_pre_sign_header: Default::default(),
|
||||
bucket_location: Default::default(),
|
||||
expires: Default::default(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let h = resp.headers();
|
||||
let has_etag = h.get("ETag").unwrap().to_str().unwrap();
|
||||
if !has_etag.is_empty() {
|
||||
return Err(std::io::Error::other(
|
||||
"get_object_attributes is not supported by the current endpoint version",
|
||||
));
|
||||
}
|
||||
|
||||
if resp.status() != http::StatusCode::OK {
|
||||
let b = resp.body_mut().store_all_unlimited().await.unwrap().to_vec();
|
||||
let err_body = String::from_utf8(b).unwrap();
|
||||
let mut er = match serde_xml_rs::from_str::<AccessControlPolicy>(&err_body) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
return Err(std::io::Error::other(er.access_control_list.permission));
|
||||
}
|
||||
|
||||
let mut oa = ObjectAttributes::new();
|
||||
oa.parse_response(&mut resp).await?;
|
||||
|
||||
Ok(oa)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// 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.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_assignments)]
|
||||
#![allow(unused_must_use)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::HeaderMap;
|
||||
use std::io::Cursor;
|
||||
#[cfg(not(windows))]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
#[cfg(not(windows))]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
#[cfg(not(windows))]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use tokio::io::BufReader;
|
||||
|
||||
use crate::client::{
|
||||
api_error_response::err_invalid_argument,
|
||||
api_get_options::GetObjectOptions,
|
||||
transition_api::{ObjectInfo, ReadCloser, ReaderImpl, RequestMetadata, TransitionClient, to_object_info},
|
||||
};
|
||||
|
||||
impl TransitionClient {
|
||||
pub async fn fget_object(
|
||||
&self,
|
||||
bucket_name: &str,
|
||||
object_name: &str,
|
||||
file_path: &str,
|
||||
opts: GetObjectOptions,
|
||||
) -> Result<(), std::io::Error> {
|
||||
match std::fs::metadata(file_path) {
|
||||
Ok(file_path_stat) => {
|
||||
let ft = file_path_stat.file_type();
|
||||
if ft.is_dir() {
|
||||
return Err(std::io::Error::other(err_invalid_argument("filename is a directory.")));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
}
|
||||
|
||||
let path = std::path::Path::new(file_path);
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Some(object_dir) = parent.file_name() {
|
||||
match std::fs::create_dir_all(object_dir) {
|
||||
Ok(_) => {
|
||||
let dir = std::path::Path::new(object_dir);
|
||||
if let Ok(dir_stat) = dir.metadata() {
|
||||
#[cfg(not(windows))]
|
||||
dir_stat.permissions().set_mode(0o700);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let object_stat = match self.stat_object(bucket_name, object_name, &opts).await {
|
||||
Ok(object_stat) => object_stat,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
|
||||
let mut file_part_path = file_path.to_string();
|
||||
file_part_path.push_str("" /*sum_sha256_hex(object_stat.etag.as_bytes())*/);
|
||||
file_part_path.push_str(".part.rustfs");
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let file_part = match std::fs::OpenOptions::new().mode(0o600).open(file_part_path.clone()) {
|
||||
Ok(file_part) => file_part,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let file_part = match std::fs::OpenOptions::new().open(file_part_path.clone()) {
|
||||
Ok(file_part) => file_part,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
|
||||
let mut close_and_remove = true;
|
||||
/*defer(|| {
|
||||
if close_and_remove {
|
||||
_ = file_part.close();
|
||||
let _ = std::fs::remove(file_part_path);
|
||||
}
|
||||
});*/
|
||||
|
||||
let st = match file_part.metadata() {
|
||||
Ok(st) => st,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
|
||||
let mut opts = opts;
|
||||
#[cfg(windows)]
|
||||
if st.file_size() > 0 {
|
||||
opts.set_range(st.file_size() as i64, 0);
|
||||
}
|
||||
|
||||
let object_reader = match self.get_object(bucket_name, object_name, &opts) {
|
||||
Ok(object_reader) => object_reader,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
|
||||
/*if let Err(err) = std::fs::copy(file_part, object_reader) {
|
||||
return Err(std::io::Error::other(err));
|
||||
}*/
|
||||
|
||||
close_and_remove = false;
|
||||
/*if let Err(err) = file_part.close() {
|
||||
return Err(std::io::Error::other(err));
|
||||
}*/
|
||||
|
||||
if let Err(err) = std::fs::rename(file_part_path, file_path) {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,9 @@ use crate::client::api_error_response::err_invalid_argument;
|
||||
#[derive(Default)]
|
||||
#[allow(dead_code)]
|
||||
pub struct AdvancedGetOptions {
|
||||
replication_deletemarker: bool,
|
||||
is_replication_ready_for_deletemarker: bool,
|
||||
replication_proxy_request: String,
|
||||
pub replication_delete_marker: bool,
|
||||
pub is_replication_ready_for_delete_marker: bool,
|
||||
pub replication_proxy_request: String,
|
||||
}
|
||||
|
||||
pub struct GetObjectOptions {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -25,7 +24,6 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use time::{Duration, OffsetDateTime, macros::format_description};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use rustfs_utils::hasher::Hasher;
|
||||
use s3s::dto::{ObjectLockLegalHoldStatus, ObjectLockRetentionMode, ReplicationStatus};
|
||||
use s3s::header::{
|
||||
X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE, X_AMZ_REPLICATION_STATUS,
|
||||
@@ -364,18 +362,14 @@ impl TransitionClient {
|
||||
if opts.send_content_md5 {
|
||||
let mut md5_hasher = self.md5_hasher.lock().unwrap();
|
||||
let hash = md5_hasher.as_mut().expect("err");
|
||||
hash.write(&buf[..length]);
|
||||
md5_base64 = base64_encode(hash.sum().as_bytes());
|
||||
let hash = hash.hash_encode(&buf[..length]);
|
||||
md5_base64 = base64_encode(hash.as_ref());
|
||||
} else {
|
||||
let csum;
|
||||
{
|
||||
let mut crc = opts.auto_checksum.hasher()?;
|
||||
crc.reset();
|
||||
crc.write(&buf[..length]);
|
||||
csum = crc.sum();
|
||||
}
|
||||
let mut crc = opts.auto_checksum.hasher()?;
|
||||
let csum = crc.hash_encode(&buf[..length]);
|
||||
|
||||
if let Ok(header_name) = HeaderName::from_bytes(opts.auto_checksum.key().as_bytes()) {
|
||||
custom_header.insert(header_name, base64_encode(csum.as_bytes()).parse().unwrap());
|
||||
custom_header.insert(header_name, base64_encode(csum.as_ref()).parse().expect("err"));
|
||||
} else {
|
||||
warn!("Invalid header name: {}", opts.auto_checksum.key());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -31,7 +30,6 @@ use tracing::{error, info};
|
||||
use url::form_urlencoded::Serializer;
|
||||
use uuid::Uuid;
|
||||
|
||||
use rustfs_utils::hasher::Hasher;
|
||||
use s3s::header::{X_AMZ_EXPIRATION, X_AMZ_VERSION_ID};
|
||||
use s3s::{Body, dto::StreamingBlob};
|
||||
//use crate::disk::{Reader, BufferReader};
|
||||
@@ -117,8 +115,8 @@ impl TransitionClient {
|
||||
let length = buf.len();
|
||||
|
||||
for (k, v) in hash_algos.iter_mut() {
|
||||
v.write(&buf[..length]);
|
||||
hash_sums.insert(k.to_string(), Vec::try_from(v.sum().as_bytes()).unwrap());
|
||||
let hash = v.hash_encode(&buf[..length]);
|
||||
hash_sums.insert(k.to_string(), hash.as_ref().to_vec());
|
||||
}
|
||||
|
||||
//let rd = newHook(bytes.NewReader(buf[..length]), opts.progress);
|
||||
@@ -134,15 +132,11 @@ impl TransitionClient {
|
||||
sha256_hex = hex_simd::encode_to_string(hash_sums["sha256"].clone(), hex_simd::AsciiCase::Lower);
|
||||
//}
|
||||
if hash_sums.len() == 0 {
|
||||
let csum;
|
||||
{
|
||||
let mut crc = opts.auto_checksum.hasher()?;
|
||||
crc.reset();
|
||||
crc.write(&buf[..length]);
|
||||
csum = crc.sum();
|
||||
}
|
||||
let mut crc = opts.auto_checksum.hasher()?;
|
||||
let csum = crc.hash_encode(&buf[..length]);
|
||||
|
||||
if let Ok(header_name) = HeaderName::from_bytes(opts.auto_checksum.key().as_bytes()) {
|
||||
custom_header.insert(header_name, base64_encode(csum.as_bytes()).parse().expect("err"));
|
||||
custom_header.insert(header_name, base64_encode(csum.as_ref()).parse().expect("err"));
|
||||
} else {
|
||||
warn!("Invalid header name: {}", opts.auto_checksum.key());
|
||||
}
|
||||
@@ -297,8 +291,6 @@ impl TransitionClient {
|
||||
};
|
||||
|
||||
let resp = self.execute_method(http::Method::PUT, &mut req_metadata).await?;
|
||||
//defer closeResponse(resp)
|
||||
//if resp.is_none() {
|
||||
if resp.status() != StatusCode::OK {
|
||||
return Err(std::io::Error::other(http_resp_to_error_response(
|
||||
resp,
|
||||
@@ -366,13 +358,13 @@ impl TransitionClient {
|
||||
bucket_name: bucket_name.to_string(),
|
||||
object_name: object_name.to_string(),
|
||||
query_values: url_values,
|
||||
custom_header: headers,
|
||||
content_body: ReaderImpl::Body(complete_multipart_upload_buffer),
|
||||
content_length: 100, //complete_multipart_upload_bytes.len(),
|
||||
content_sha256_hex: "".to_string(), //hex_simd::encode_to_string(complete_multipart_upload_bytes, hex_simd::AsciiCase::Lower),
|
||||
custom_header: headers,
|
||||
content_md5_base64: "".to_string(),
|
||||
stream_sha256: Default::default(),
|
||||
trailer: Default::default(),
|
||||
content_md5_base64: "".to_string(),
|
||||
pre_sign_url: Default::default(),
|
||||
add_crc: Default::default(),
|
||||
extra_pre_sign_header: Default::default(),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -40,7 +39,7 @@ use crate::client::{
|
||||
constants::ISO8601_DATEFORMAT,
|
||||
transition_api::{ReaderImpl, RequestMetadata, TransitionClient, UploadInfo},
|
||||
};
|
||||
use rustfs_utils::hasher::Hasher;
|
||||
|
||||
use rustfs_utils::{crypto::base64_encode, path::trim_etag};
|
||||
use s3s::header::{X_AMZ_EXPIRATION, X_AMZ_VERSION_ID};
|
||||
|
||||
@@ -153,21 +152,16 @@ impl TransitionClient {
|
||||
if opts.send_content_md5 {
|
||||
let mut md5_hasher = self.md5_hasher.lock().unwrap();
|
||||
let md5_hash = md5_hasher.as_mut().expect("err");
|
||||
md5_hash.reset();
|
||||
md5_hash.write(&buf[..length]);
|
||||
md5_base64 = base64_encode(md5_hash.sum().as_bytes());
|
||||
let hash = md5_hash.hash_encode(&buf[..length]);
|
||||
md5_base64 = base64_encode(hash.as_ref());
|
||||
} else {
|
||||
let csum;
|
||||
{
|
||||
let mut crc = opts.auto_checksum.hasher()?;
|
||||
crc.reset();
|
||||
crc.write(&buf[..length]);
|
||||
csum = crc.sum();
|
||||
}
|
||||
if let Ok(header_name) = HeaderName::from_bytes(opts.auto_checksum.key_capitalized().as_bytes()) {
|
||||
custom_header.insert(header_name, HeaderValue::from_str(&base64_encode(csum.as_bytes())).expect("err"));
|
||||
let mut crc = opts.auto_checksum.hasher()?;
|
||||
let csum = crc.hash_encode(&buf[..length]);
|
||||
|
||||
if let Ok(header_name) = HeaderName::from_bytes(opts.auto_checksum.key().as_bytes()) {
|
||||
custom_header.insert(header_name, base64_encode(csum.as_ref()).parse().expect("err"));
|
||||
} else {
|
||||
warn!("Invalid header name: {}", opts.auto_checksum.key_capitalized());
|
||||
warn!("Invalid header name: {}", opts.auto_checksum.key());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,17 +302,11 @@ impl TransitionClient {
|
||||
|
||||
let mut custom_header = HeaderMap::new();
|
||||
if !opts.send_content_md5 {
|
||||
let csum;
|
||||
{
|
||||
let mut crc = opts.auto_checksum.hasher()?;
|
||||
crc.reset();
|
||||
crc.write(&buf[..length]);
|
||||
csum = crc.sum();
|
||||
}
|
||||
let mut crc = opts.auto_checksum.hasher()?;
|
||||
let csum = crc.hash_encode(&buf[..length]);
|
||||
|
||||
if let Ok(header_name) = HeaderName::from_bytes(opts.auto_checksum.key().as_bytes()) {
|
||||
if let Ok(header_value) = HeaderValue::from_str(&base64_encode(csum.as_bytes())) {
|
||||
custom_header.insert(header_name, header_value);
|
||||
}
|
||||
custom_header.insert(header_name, base64_encode(csum.as_ref()).parse().expect("err"));
|
||||
} else {
|
||||
warn!("Invalid header name: {}", opts.auto_checksum.key());
|
||||
}
|
||||
@@ -334,8 +322,8 @@ impl TransitionClient {
|
||||
if opts.send_content_md5 {
|
||||
let mut md5_hasher = clone_self.md5_hasher.lock().unwrap();
|
||||
let md5_hash = md5_hasher.as_mut().expect("err");
|
||||
md5_hash.write(&buf[..length]);
|
||||
md5_base64 = base64_encode(md5_hash.sum().as_bytes());
|
||||
let hash = md5_hash.hash_encode(&buf[..length]);
|
||||
md5_base64 = base64_encode(hash.as_ref());
|
||||
}
|
||||
|
||||
//defer wg.Done()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -21,6 +20,7 @@
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::{HeaderMap, HeaderValue, Method, StatusCode};
|
||||
use rustfs_utils::{HashAlgorithm, crypto::base64_encode};
|
||||
use s3s::S3ErrorCode;
|
||||
use s3s::dto::ReplicationStatus;
|
||||
use s3s::header::X_AMZ_BYPASS_GOVERNANCE_RETENTION;
|
||||
@@ -38,7 +38,6 @@ use crate::{
|
||||
store_api::{GetObjectReader, ObjectInfo, StorageAPI},
|
||||
};
|
||||
use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH;
|
||||
use rustfs_utils::hasher::{sum_md5_base64, sum_sha256_hex};
|
||||
|
||||
pub struct RemoveBucketOptions {
|
||||
_forced_elete: bool,
|
||||
@@ -330,8 +329,8 @@ impl TransitionClient {
|
||||
query_values: url_values.clone(),
|
||||
content_body: ReaderImpl::Body(Bytes::from(remove_bytes.clone())),
|
||||
content_length: remove_bytes.len() as i64,
|
||||
content_md5_base64: sum_md5_base64(&remove_bytes),
|
||||
content_sha256_hex: sum_sha256_hex(&remove_bytes),
|
||||
content_md5_base64: base64_encode(&HashAlgorithm::Md5.hash_encode(&remove_bytes).as_ref()),
|
||||
content_sha256_hex: base64_encode(&HashAlgorithm::SHA256.hash_encode(&remove_bytes).as_ref()),
|
||||
custom_header: headers,
|
||||
object_name: "".to_string(),
|
||||
stream_sha256: false,
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// 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.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_assignments)]
|
||||
#![allow(unused_must_use)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::HeaderMap;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::BufReader;
|
||||
|
||||
use crate::client::{
|
||||
api_error_response::{err_invalid_argument, http_resp_to_error_response},
|
||||
api_get_object_acl::AccessControlList,
|
||||
api_get_options::GetObjectOptions,
|
||||
transition_api::{ObjectInfo, ReadCloser, ReaderImpl, RequestMetadata, TransitionClient, to_object_info},
|
||||
};
|
||||
|
||||
const TIER_STANDARD: &str = "Standard";
|
||||
const TIER_BULK: &str = "Bulk";
|
||||
const TIER_EXPEDITED: &str = "Expedited";
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct GlacierJobParameters {
|
||||
pub tier: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Encryption {
|
||||
pub encryption_type: String,
|
||||
pub kms_context: String,
|
||||
pub kms_key_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct MetadataEntry {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct S3 {
|
||||
pub access_control_list: AccessControlList,
|
||||
pub bucket_name: String,
|
||||
pub prefix: String,
|
||||
pub canned_acl: String,
|
||||
pub encryption: Encryption,
|
||||
pub storage_class: String,
|
||||
//tagging: Tags,
|
||||
pub user_metadata: MetadataEntry,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct SelectParameters {
|
||||
pub expression_type: String,
|
||||
pub expression: String,
|
||||
//input_serialization: SelectObjectInputSerialization,
|
||||
//output_serialization: SelectObjectOutputSerialization,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct OutputLocation(pub S3);
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub struct RestoreRequest {
|
||||
pub restore_type: String,
|
||||
pub tier: String,
|
||||
pub days: i64,
|
||||
pub glacier_job_parameters: GlacierJobParameters,
|
||||
pub description: String,
|
||||
pub select_parameters: SelectParameters,
|
||||
pub output_location: OutputLocation,
|
||||
}
|
||||
|
||||
impl RestoreRequest {
|
||||
fn set_days(&mut self, v: i64) {
|
||||
self.days = v;
|
||||
}
|
||||
|
||||
fn set_glacier_job_parameters(&mut self, v: GlacierJobParameters) {
|
||||
self.glacier_job_parameters = v;
|
||||
}
|
||||
|
||||
fn set_type(&mut self, v: &str) {
|
||||
self.restore_type = v.to_string();
|
||||
}
|
||||
|
||||
fn set_tier(&mut self, v: &str) {
|
||||
self.tier = v.to_string();
|
||||
}
|
||||
|
||||
fn set_description(&mut self, v: &str) {
|
||||
self.description = v.to_string();
|
||||
}
|
||||
|
||||
fn set_select_parameters(&mut self, v: SelectParameters) {
|
||||
self.select_parameters = v;
|
||||
}
|
||||
|
||||
fn set_output_location(&mut self, v: OutputLocation) {
|
||||
self.output_location = v;
|
||||
}
|
||||
}
|
||||
|
||||
impl TransitionClient {
|
||||
pub async fn restore_object(
|
||||
&self,
|
||||
bucket_name: &str,
|
||||
object_name: &str,
|
||||
version_id: &str,
|
||||
restore_req: &RestoreRequest,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let restore_request = match serde_xml_rs::to_string(restore_req) {
|
||||
Ok(buf) => buf,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::other(e));
|
||||
}
|
||||
};
|
||||
let restore_request_bytes = restore_request.as_bytes().to_vec();
|
||||
|
||||
let mut url_values = HashMap::new();
|
||||
url_values.insert("restore".to_string(), "".to_string());
|
||||
if version_id != "" {
|
||||
url_values.insert("versionId".to_string(), version_id.to_string());
|
||||
}
|
||||
|
||||
let restore_request_buffer = Bytes::from(restore_request_bytes.clone());
|
||||
let resp = self
|
||||
.execute_method(
|
||||
http::Method::HEAD,
|
||||
&mut RequestMetadata {
|
||||
bucket_name: bucket_name.to_string(),
|
||||
object_name: object_name.to_string(),
|
||||
query_values: url_values,
|
||||
custom_header: HeaderMap::new(),
|
||||
content_sha256_hex: "".to_string(), //sum_sha256_hex(&restore_request_bytes),
|
||||
content_md5_base64: "".to_string(), //sum_md5_base64(&restore_request_bytes),
|
||||
content_body: ReaderImpl::Body(restore_request_buffer),
|
||||
content_length: restore_request_bytes.len() as i64,
|
||||
stream_sha256: false,
|
||||
trailer: HeaderMap::new(),
|
||||
pre_sign_url: Default::default(),
|
||||
add_crc: Default::default(),
|
||||
extra_pre_sign_header: Default::default(),
|
||||
bucket_location: Default::default(),
|
||||
expires: Default::default(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let b = resp.body().bytes().expect("err").to_vec();
|
||||
if resp.status() != http::StatusCode::ACCEPTED && resp.status() != http::StatusCode::OK {
|
||||
return Err(std::io::Error::other(http_resp_to_error_response(resp, b, bucket_name, "")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// 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.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_assignments)]
|
||||
#![allow(unused_must_use)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use rustfs_utils::EMPTY_STRING_SHA256_HASH;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use tokio::io::BufReader;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::client::{
|
||||
api_error_response::{ErrorResponse, err_invalid_argument, http_resp_to_error_response},
|
||||
api_get_options::GetObjectOptions,
|
||||
transition_api::{ObjectInfo, ReadCloser, ReaderImpl, RequestMetadata, TransitionClient, to_object_info},
|
||||
};
|
||||
use s3s::header::{X_AMZ_DELETE_MARKER, X_AMZ_VERSION_ID};
|
||||
|
||||
impl TransitionClient {
|
||||
pub async fn bucket_exists(&self, bucket_name: &str) -> Result<bool, std::io::Error> {
|
||||
let resp = self
|
||||
.execute_method(
|
||||
http::Method::HEAD,
|
||||
&mut RequestMetadata {
|
||||
bucket_name: bucket_name.to_string(),
|
||||
object_name: "".to_string(),
|
||||
query_values: HashMap::new(),
|
||||
custom_header: HeaderMap::new(),
|
||||
content_sha256_hex: EMPTY_STRING_SHA256_HASH.to_string(),
|
||||
content_md5_base64: "".to_string(),
|
||||
content_body: ReaderImpl::Body(Bytes::new()),
|
||||
content_length: 0,
|
||||
stream_sha256: false,
|
||||
trailer: HeaderMap::new(),
|
||||
pre_sign_url: Default::default(),
|
||||
add_crc: Default::default(),
|
||||
extra_pre_sign_header: Default::default(),
|
||||
bucket_location: Default::default(),
|
||||
expires: Default::default(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(resp) = resp {
|
||||
let b = resp.body().bytes().expect("err").to_vec();
|
||||
let resperr = http_resp_to_error_response(resp, b, bucket_name, "");
|
||||
/*if to_error_response(resperr).code == "NoSuchBucket" {
|
||||
return Ok(false);
|
||||
}
|
||||
if resp.status_code() != http::StatusCode::OK {
|
||||
return Ok(false);
|
||||
}*/
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn stat_object(
|
||||
&self,
|
||||
bucket_name: &str,
|
||||
object_name: &str,
|
||||
opts: &GetObjectOptions,
|
||||
) -> Result<ObjectInfo, std::io::Error> {
|
||||
let mut headers = opts.header();
|
||||
if opts.internal.replication_delete_marker {
|
||||
headers.insert("X-Source-DeleteMarker", HeaderValue::from_str("true").unwrap());
|
||||
}
|
||||
if opts.internal.is_replication_ready_for_delete_marker {
|
||||
headers.insert("X-Check-Replication-Ready", HeaderValue::from_str("true").unwrap());
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.execute_method(
|
||||
http::Method::HEAD,
|
||||
&mut RequestMetadata {
|
||||
bucket_name: bucket_name.to_string(),
|
||||
object_name: object_name.to_string(),
|
||||
query_values: opts.to_query_values(),
|
||||
custom_header: headers,
|
||||
content_sha256_hex: EMPTY_STRING_SHA256_HASH.to_string(),
|
||||
content_md5_base64: "".to_string(),
|
||||
content_body: ReaderImpl::Body(Bytes::new()),
|
||||
content_length: 0,
|
||||
stream_sha256: false,
|
||||
trailer: HeaderMap::new(),
|
||||
pre_sign_url: Default::default(),
|
||||
add_crc: Default::default(),
|
||||
extra_pre_sign_header: Default::default(),
|
||||
bucket_location: Default::default(),
|
||||
expires: Default::default(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
match resp {
|
||||
Ok(resp) => {
|
||||
let h = resp.headers();
|
||||
let delete_marker = if let Some(x_amz_delete_marker) = h.get(X_AMZ_DELETE_MARKER.as_str()) {
|
||||
x_amz_delete_marker.to_str().unwrap() == "true"
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let replication_ready = if let Some(x_amz_delete_marker) = h.get("X-Replication-Ready") {
|
||||
x_amz_delete_marker.to_str().unwrap() == "true"
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if resp.status() != http::StatusCode::OK && resp.status() != http::StatusCode::PARTIAL_CONTENT {
|
||||
if resp.status() == http::StatusCode::METHOD_NOT_ALLOWED && opts.version_id != "" && delete_marker {
|
||||
let err_resp = ErrorResponse {
|
||||
status_code: resp.status(),
|
||||
code: s3s::S3ErrorCode::MethodNotAllowed,
|
||||
message: "the specified method is not allowed against this resource.".to_string(),
|
||||
bucket_name: bucket_name.to_string(),
|
||||
key: object_name.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
return Ok(ObjectInfo {
|
||||
version_id: match Uuid::from_str(h.get(X_AMZ_VERSION_ID).unwrap().to_str().unwrap()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::other(e));
|
||||
}
|
||||
},
|
||||
is_delete_marker: delete_marker,
|
||||
..Default::default()
|
||||
});
|
||||
//err_resp
|
||||
}
|
||||
return Ok(ObjectInfo {
|
||||
version_id: match Uuid::from_str(h.get(X_AMZ_VERSION_ID).unwrap().to_str().unwrap()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::other(e));
|
||||
}
|
||||
},
|
||||
is_delete_marker: delete_marker,
|
||||
replication_ready: replication_ready,
|
||||
..Default::default()
|
||||
});
|
||||
//http_resp_to_error_response(resp, bucket_name, object_name)
|
||||
}
|
||||
|
||||
Ok(to_object_info(bucket_name, object_name, h).unwrap())
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -31,7 +30,6 @@ use crate::client::{
|
||||
transition_api::{Document, TransitionClient},
|
||||
};
|
||||
use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH;
|
||||
use rustfs_utils::hasher::{Hasher, Sha256};
|
||||
use s3s::Body;
|
||||
use s3s::S3ErrorCode;
|
||||
|
||||
@@ -125,9 +123,11 @@ impl TransitionClient {
|
||||
url_str = target_url.to_string();
|
||||
}
|
||||
|
||||
let mut req_builder = Request::builder().method(http::Method::GET).uri(url_str);
|
||||
let Ok(mut req) = Request::builder().method(http::Method::GET).uri(url_str).body(Body::empty()) else {
|
||||
return Err(std::io::Error::other("create request error"));
|
||||
};
|
||||
|
||||
self.set_user_agent(&mut req_builder);
|
||||
self.set_user_agent(&mut req);
|
||||
|
||||
let value;
|
||||
{
|
||||
@@ -154,22 +154,12 @@ impl TransitionClient {
|
||||
}
|
||||
|
||||
if signer_type == SignatureType::SignatureAnonymous {
|
||||
let req = match req_builder.body(Body::empty()) {
|
||||
Ok(req) => return Ok(req),
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
return Ok(req);
|
||||
}
|
||||
|
||||
if signer_type == SignatureType::SignatureV2 {
|
||||
let req_builder = rustfs_signer::sign_v2(req_builder, 0, &access_key_id, &secret_access_key, is_virtual_style);
|
||||
let req = match req_builder.body(Body::empty()) {
|
||||
Ok(req) => return Ok(req),
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
let req = rustfs_signer::sign_v2(req, 0, &access_key_id, &secret_access_key, is_virtual_style);
|
||||
return Ok(req);
|
||||
}
|
||||
|
||||
let mut content_sha256 = EMPTY_STRING_SHA256_HASH.to_string();
|
||||
@@ -177,17 +167,10 @@ impl TransitionClient {
|
||||
content_sha256 = UNSIGNED_PAYLOAD.to_string();
|
||||
}
|
||||
|
||||
req_builder
|
||||
.headers_mut()
|
||||
.expect("err")
|
||||
req.headers_mut()
|
||||
.insert("X-Amz-Content-Sha256", content_sha256.parse().unwrap());
|
||||
let req_builder = rustfs_signer::sign_v4(req_builder, 0, &access_key_id, &secret_access_key, &session_token, "us-east-1");
|
||||
let req = match req_builder.body(Body::empty()) {
|
||||
Ok(req) => return Ok(req),
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
let req = rustfs_signer::sign_v4(req, 0, &access_key_id, &secret_access_key, &session_token, "us-east-1");
|
||||
Ok(req)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ pub mod admin_handler_utils;
|
||||
pub mod api_bucket_policy;
|
||||
pub mod api_error_response;
|
||||
pub mod api_get_object;
|
||||
pub mod api_get_object_acl;
|
||||
pub mod api_get_object_attributes;
|
||||
pub mod api_get_object_file;
|
||||
pub mod api_get_options;
|
||||
pub mod api_list;
|
||||
pub mod api_put_object;
|
||||
@@ -23,7 +26,9 @@ pub mod api_put_object_common;
|
||||
pub mod api_put_object_multipart;
|
||||
pub mod api_put_object_streaming;
|
||||
pub mod api_remove;
|
||||
pub mod api_restore;
|
||||
pub mod api_s3_datatypes;
|
||||
pub mod api_stat;
|
||||
pub mod bucket_cache;
|
||||
pub mod constants;
|
||||
pub mod credentials;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::map_entry)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -28,8 +27,12 @@ use http::{
|
||||
};
|
||||
use hyper_rustls::{ConfigBuilderExt, HttpsConnector};
|
||||
use hyper_util::{client::legacy::Client, client::legacy::connect::HttpConnector, rt::TokioExecutor};
|
||||
use md5::Digest;
|
||||
use md5::Md5;
|
||||
use rand::Rng;
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::io::Cursor;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
@@ -60,7 +63,6 @@ use crate::client::{
|
||||
};
|
||||
use crate::{checksum::ChecksumMode, store_api::GetObjectReader};
|
||||
use rustfs_rio::HashReader;
|
||||
use rustfs_utils::hasher::{MD5, Sha256};
|
||||
use rustfs_utils::{
|
||||
net::get_endpoint_url,
|
||||
retry::{MAX_RETRY, new_retry_timer},
|
||||
@@ -69,7 +71,6 @@ use s3s::S3ErrorCode;
|
||||
use s3s::dto::ReplicationStatus;
|
||||
use s3s::{Body, dto::Owner};
|
||||
|
||||
const _C_USER_AGENT_PREFIX: &str = "RustFS (linux; x86)";
|
||||
const C_USER_AGENT: &str = "RustFS (linux; x86)";
|
||||
|
||||
const SUCCESS_STATUS: [StatusCode; 3] = [StatusCode::OK, StatusCode::NO_CONTENT, StatusCode::PARTIAL_CONTENT];
|
||||
@@ -90,22 +91,18 @@ pub struct TransitionClient {
|
||||
pub endpoint_url: Url,
|
||||
pub creds_provider: Arc<Mutex<Credentials<Static>>>,
|
||||
pub override_signer_type: SignatureType,
|
||||
/*app_info: TODO*/
|
||||
pub secure: bool,
|
||||
pub http_client: Client<HttpsConnector<HttpConnector>, Body>,
|
||||
//pub http_trace: Httptrace.ClientTrace,
|
||||
pub bucket_loc_cache: Arc<Mutex<BucketLocationCache>>,
|
||||
pub is_trace_enabled: Arc<Mutex<bool>>,
|
||||
pub trace_errors_only: Arc<Mutex<bool>>,
|
||||
//pub trace_output: io.Writer,
|
||||
pub s3_accelerate_endpoint: Arc<Mutex<String>>,
|
||||
pub s3_dual_stack_enabled: Arc<Mutex<bool>>,
|
||||
pub region: String,
|
||||
pub random: u64,
|
||||
pub lookup: BucketLookupType,
|
||||
//pub lookupFn: func(u url.URL, bucketName string) BucketLookupType,
|
||||
pub md5_hasher: Arc<Mutex<Option<MD5>>>,
|
||||
pub sha256_hasher: Option<Sha256>,
|
||||
pub md5_hasher: Arc<Mutex<Option<HashAlgorithm>>>,
|
||||
pub sha256_hasher: Option<HashAlgorithm>,
|
||||
pub health_status: AtomicI32,
|
||||
pub trailing_header_support: bool,
|
||||
pub max_retries: i64,
|
||||
@@ -115,15 +112,11 @@ pub struct TransitionClient {
|
||||
pub struct Options {
|
||||
pub creds: Credentials<Static>,
|
||||
pub secure: bool,
|
||||
//pub transport: http.RoundTripper,
|
||||
//pub trace: *httptrace.ClientTrace,
|
||||
pub region: String,
|
||||
pub bucket_lookup: BucketLookupType,
|
||||
//pub custom_region_via_url: func(u url.URL) string,
|
||||
//pub bucket_lookup_via_url: func(u url.URL, bucketName string) BucketLookupType,
|
||||
pub trailing_headers: bool,
|
||||
pub custom_md5: Option<MD5>,
|
||||
pub custom_sha256: Option<Sha256>,
|
||||
pub custom_md5: Option<HashAlgorithm>,
|
||||
pub custom_sha256: Option<HashAlgorithm>,
|
||||
pub max_retries: i64,
|
||||
}
|
||||
|
||||
@@ -145,8 +138,6 @@ impl TransitionClient {
|
||||
async fn private_new(endpoint: &str, opts: Options) -> Result<TransitionClient, std::io::Error> {
|
||||
let endpoint_url = get_endpoint_url(endpoint, opts.secure)?;
|
||||
|
||||
//let jar = cookiejar.New(cookiejar.Options{PublicSuffixList: publicsuffix.List})?;
|
||||
|
||||
//#[cfg(feature = "ring")]
|
||||
//let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
//#[cfg(feature = "aws-lc-rs")]
|
||||
@@ -154,9 +145,6 @@ impl TransitionClient {
|
||||
|
||||
let scheme = endpoint_url.scheme();
|
||||
let client;
|
||||
//if scheme == "https" {
|
||||
// client = Client::builder(TokioExecutor::new()).build_http();
|
||||
//} else {
|
||||
let tls = rustls::ClientConfig::builder().with_native_roots()?.with_no_client_auth();
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_tls_config(tls)
|
||||
@@ -164,7 +152,6 @@ impl TransitionClient {
|
||||
.enable_http1()
|
||||
.build();
|
||||
client = Client::builder(TokioExecutor::new()).build(https);
|
||||
//}
|
||||
|
||||
let mut clnt = TransitionClient {
|
||||
endpoint_url,
|
||||
@@ -190,11 +177,11 @@ impl TransitionClient {
|
||||
{
|
||||
let mut md5_hasher = clnt.md5_hasher.lock().unwrap();
|
||||
if md5_hasher.is_none() {
|
||||
*md5_hasher = Some(MD5::new());
|
||||
*md5_hasher = Some(HashAlgorithm::Md5);
|
||||
}
|
||||
}
|
||||
if clnt.sha256_hasher.is_none() {
|
||||
clnt.sha256_hasher = Some(Sha256::new());
|
||||
clnt.sha256_hasher = Some(HashAlgorithm::SHA256);
|
||||
}
|
||||
|
||||
clnt.trailing_header_support = opts.trailing_headers && clnt.override_signer_type == SignatureType::SignatureV4;
|
||||
@@ -210,13 +197,6 @@ impl TransitionClient {
|
||||
self.endpoint_url.clone()
|
||||
}
|
||||
|
||||
fn set_appinfo(&self, app_name: &str, app_version: &str) {
|
||||
/*if app_name != "" && app_version != "" {
|
||||
self.appInfo.app_name = app_name
|
||||
self.appInfo.app_version = app_version
|
||||
}*/
|
||||
}
|
||||
|
||||
fn trace_errors_only_off(&self) {
|
||||
let mut trace_errors_only = self.trace_errors_only.lock().unwrap();
|
||||
*trace_errors_only = false;
|
||||
@@ -241,8 +221,8 @@ impl TransitionClient {
|
||||
&self,
|
||||
is_md5_requested: bool,
|
||||
is_sha256_requested: bool,
|
||||
) -> (HashMap<String, MD5>, HashMap<String, Vec<u8>>) {
|
||||
todo!();
|
||||
) -> (HashMap<String, HashAlgorithm>, HashMap<String, Vec<u8>>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn is_online(&self) -> bool {
|
||||
@@ -265,6 +245,7 @@ impl TransitionClient {
|
||||
fn dump_http(&self, req: &http::Request<Body>, resp: &http::Response<Body>) -> Result<(), std::io::Error> {
|
||||
let mut resp_trace: Vec<u8>;
|
||||
|
||||
//info!("{}{}", self.trace_output, "---------BEGIN-HTTP---------");
|
||||
//info!("{}{}", self.trace_output, "---------END-HTTP---------");
|
||||
|
||||
Ok(())
|
||||
@@ -335,7 +316,7 @@ impl TransitionClient {
|
||||
//let mut retry_timer = RetryTimer::new();
|
||||
//while let Some(v) = retry_timer.next().await {
|
||||
for _ in [1; 1]
|
||||
/*new_retry_timer(req_retry, DefaultRetryUnit, DefaultRetryCap, MaxJitter)*/
|
||||
/*new_retry_timer(req_retry, default_retry_unit, default_retry_cap, max_jitter)*/
|
||||
{
|
||||
let req = self.new_request(method, metadata).await?;
|
||||
|
||||
@@ -406,7 +387,13 @@ impl TransitionClient {
|
||||
&metadata.query_values,
|
||||
)?;
|
||||
|
||||
let mut req_builder = Request::builder().method(method).uri(target_url.to_string());
|
||||
let Ok(mut req) = Request::builder()
|
||||
.method(method)
|
||||
.uri(target_url.to_string())
|
||||
.body(Body::empty())
|
||||
else {
|
||||
return Err(std::io::Error::other("create request error"));
|
||||
};
|
||||
|
||||
let value;
|
||||
{
|
||||
@@ -430,30 +417,25 @@ impl TransitionClient {
|
||||
if metadata.expires != 0 && metadata.pre_sign_url {
|
||||
if signer_type == SignatureType::SignatureAnonymous {
|
||||
return Err(std::io::Error::other(err_invalid_argument(
|
||||
"Presigned URLs cannot be generated with anonymous credentials.",
|
||||
"presigned urls cannot be generated with anonymous credentials.",
|
||||
)));
|
||||
}
|
||||
if metadata.extra_pre_sign_header.is_some() {
|
||||
if signer_type == SignatureType::SignatureV2 {
|
||||
return Err(std::io::Error::other(err_invalid_argument(
|
||||
"Extra signed headers for Presign with Signature V2 is not supported.",
|
||||
"extra signed headers for presign with signature v2 is not supported.",
|
||||
)));
|
||||
}
|
||||
let headers = req.headers_mut();
|
||||
for (k, v) in metadata.extra_pre_sign_header.as_ref().unwrap() {
|
||||
req_builder = req_builder.header(k, v);
|
||||
headers.insert(k, v.clone());
|
||||
}
|
||||
}
|
||||
if signer_type == SignatureType::SignatureV2 {
|
||||
req_builder = rustfs_signer::pre_sign_v2(
|
||||
req_builder,
|
||||
&access_key_id,
|
||||
&secret_access_key,
|
||||
metadata.expires,
|
||||
is_virtual_host,
|
||||
);
|
||||
req = rustfs_signer::pre_sign_v2(req, &access_key_id, &secret_access_key, metadata.expires, is_virtual_host);
|
||||
} else if signer_type == SignatureType::SignatureV4 {
|
||||
req_builder = rustfs_signer::pre_sign_v4(
|
||||
req_builder,
|
||||
req = rustfs_signer::pre_sign_v4(
|
||||
req,
|
||||
&access_key_id,
|
||||
&secret_access_key,
|
||||
&session_token,
|
||||
@@ -462,57 +444,38 @@ impl TransitionClient {
|
||||
OffsetDateTime::now_utc(),
|
||||
);
|
||||
}
|
||||
let req = match req_builder.body(Body::empty()) {
|
||||
Ok(req) => req,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
return Ok(req);
|
||||
}
|
||||
|
||||
self.set_user_agent(&mut req_builder);
|
||||
self.set_user_agent(&mut req);
|
||||
|
||||
for (k, v) in metadata.custom_header.clone() {
|
||||
req_builder.headers_mut().expect("err").insert(k.expect("err"), v);
|
||||
req.headers_mut().insert(k.expect("err"), v);
|
||||
}
|
||||
|
||||
//req.content_length = metadata.content_length;
|
||||
if metadata.content_length <= -1 {
|
||||
let chunked_value = HeaderValue::from_str(&vec!["chunked"].join(",")).expect("err");
|
||||
req_builder
|
||||
.headers_mut()
|
||||
.expect("err")
|
||||
.insert(http::header::TRANSFER_ENCODING, chunked_value);
|
||||
req.headers_mut().insert(http::header::TRANSFER_ENCODING, chunked_value);
|
||||
}
|
||||
|
||||
if metadata.content_md5_base64.len() > 0 {
|
||||
let md5_value = HeaderValue::from_str(&metadata.content_md5_base64).expect("err");
|
||||
req_builder.headers_mut().expect("err").insert("Content-Md5", md5_value);
|
||||
req.headers_mut().insert("Content-Md5", md5_value);
|
||||
}
|
||||
|
||||
if signer_type == SignatureType::SignatureAnonymous {
|
||||
let req = match req_builder.body(Body::empty()) {
|
||||
Ok(req) => req,
|
||||
Err(err) => {
|
||||
return Err(std::io::Error::other(err));
|
||||
}
|
||||
};
|
||||
return Ok(req);
|
||||
}
|
||||
|
||||
if signer_type == SignatureType::SignatureV2 {
|
||||
req_builder =
|
||||
rustfs_signer::sign_v2(req_builder, metadata.content_length, &access_key_id, &secret_access_key, is_virtual_host);
|
||||
req = rustfs_signer::sign_v2(req, metadata.content_length, &access_key_id, &secret_access_key, is_virtual_host);
|
||||
} else if metadata.stream_sha256 && !self.secure {
|
||||
if metadata.trailer.len() > 0 {
|
||||
//req.Trailer = metadata.trailer;
|
||||
for (_, v) in &metadata.trailer {
|
||||
req_builder = req_builder.header(http::header::TRAILER, v.clone());
|
||||
req.headers_mut().insert(http::header::TRAILER, v.clone());
|
||||
}
|
||||
}
|
||||
//req_builder = rustfs_signer::streaming_sign_v4(req_builder, &access_key_id,
|
||||
// &secret_access_key, &session_token, &location, metadata.content_length, OffsetDateTime::now_utc(), self.sha256_hasher());
|
||||
} else {
|
||||
let mut sha_header = UNSIGNED_PAYLOAD.to_string();
|
||||
if metadata.content_sha256_hex != "" {
|
||||
@@ -523,11 +486,11 @@ impl TransitionClient {
|
||||
} else if metadata.trailer.len() > 0 {
|
||||
sha_header = UNSIGNED_PAYLOAD_TRAILER.to_string();
|
||||
}
|
||||
req_builder = req_builder
|
||||
.header::<HeaderName, HeaderValue>("X-Amz-Content-Sha256".parse().unwrap(), sha_header.parse().expect("err"));
|
||||
req.headers_mut()
|
||||
.insert("X-Amz-Content-Sha256".parse::<HeaderName>().unwrap(), sha_header.parse().expect("err"));
|
||||
|
||||
req_builder = rustfs_signer::sign_v4_trailer(
|
||||
req_builder,
|
||||
req = rustfs_signer::sign_v4_trailer(
|
||||
req,
|
||||
&access_key_id,
|
||||
&secret_access_key,
|
||||
&session_token,
|
||||
@@ -536,33 +499,23 @@ impl TransitionClient {
|
||||
);
|
||||
}
|
||||
|
||||
let req;
|
||||
if metadata.content_length == 0 {
|
||||
req = req_builder.body(Body::empty());
|
||||
} else {
|
||||
if metadata.content_length > 0 {
|
||||
match &mut metadata.content_body {
|
||||
ReaderImpl::Body(content_body) => {
|
||||
req = req_builder.body(Body::from(content_body.clone()));
|
||||
*req.body_mut() = Body::from(content_body.clone());
|
||||
}
|
||||
ReaderImpl::ObjectBody(content_body) => {
|
||||
req = req_builder.body(Body::from(content_body.read_all().await?));
|
||||
*req.body_mut() = Body::from(content_body.read_all().await?);
|
||||
}
|
||||
}
|
||||
//req = req_builder.body(s3s::Body::from(metadata.content_body.read_all().await?));
|
||||
}
|
||||
|
||||
match req {
|
||||
Ok(req) => Ok(req),
|
||||
Err(err) => Err(std::io::Error::other(err)),
|
||||
}
|
||||
Ok(req)
|
||||
}
|
||||
|
||||
pub fn set_user_agent(&self, req: &mut Builder) {
|
||||
let headers = req.headers_mut().expect("err");
|
||||
pub fn set_user_agent(&self, req: &mut Request<Body>) {
|
||||
let headers = req.headers_mut();
|
||||
headers.insert("User-Agent", C_USER_AGENT.parse().expect("err"));
|
||||
/*if self.app_info.app_name != "" && self.app_info.app_version != "" {
|
||||
headers.insert("User-Agent", C_USER_AGENT+" "+self.app_info.app_name+"/"+self.app_info.app_version);
|
||||
}*/
|
||||
}
|
||||
|
||||
fn make_target_url(
|
||||
@@ -945,7 +898,7 @@ pub struct ObjectMultipartInfo {
|
||||
pub key: String,
|
||||
pub size: i64,
|
||||
pub upload_id: String,
|
||||
//pub err error,
|
||||
//pub err: Error,
|
||||
}
|
||||
|
||||
pub struct UploadInfo {
|
||||
|
||||
@@ -178,6 +178,16 @@ pub async fn remove_bucket_target(bucket: &str, arn_str: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_bucket_targets(bucket: &str) -> Result<BucketTargets, BucketRemoteTargetNotFound> {
|
||||
if let Some(sys) = GLOBAL_Bucket_Target_Sys.get() {
|
||||
sys.list_bucket_targets(bucket).await
|
||||
} else {
|
||||
Err(BucketRemoteTargetNotFound {
|
||||
bucket: bucket.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BucketTargetSys {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ecstore::erasure_coding::Erasure;
|
||||
//! use rustfs_ecstore::erasure_coding::Erasure;
|
||||
//!
|
||||
//! let erasure = Erasure::new(4, 2, 1024); // 4 data shards, 2 parity shards, 1KB block size
|
||||
//! let data = b"hello world";
|
||||
@@ -263,7 +263,7 @@ impl ReedSolomonEncoder {
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use ecstore::erasure_coding::Erasure;
|
||||
/// use rustfs_ecstore::erasure_coding::Erasure;
|
||||
/// let erasure = Erasure::new(4, 2, 8);
|
||||
/// let data = b"hello world";
|
||||
/// let shards = erasure.encode_data(data).unwrap();
|
||||
|
||||
@@ -103,7 +103,7 @@ use tokio::{
|
||||
},
|
||||
time::sleep,
|
||||
};
|
||||
use tracing::{error, info};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
const DATA_SCANNER_SLEEP_PER_FOLDER: Duration = Duration::from_millis(1); // Time to wait between folders.
|
||||
const DATA_USAGE_UPDATE_DIR_CYCLES: u32 = 16; // Visit all folders every n cycles.
|
||||
@@ -741,13 +741,18 @@ impl ScannerItem {
|
||||
|
||||
// Create a mutable clone if you need to modify fields
|
||||
let mut oi = oi.clone();
|
||||
oi.replication_status = ReplicationStatusType::from(
|
||||
oi.user_defined
|
||||
.get("x-amz-bucket-replication-status")
|
||||
.unwrap_or(&"PENDING".to_string()),
|
||||
);
|
||||
info!("apply status is: {:?}", oi.replication_status);
|
||||
self.heal_replication(&oi, _size_s).await;
|
||||
|
||||
let versioned = BucketVersioningSys::prefix_enabled(&oi.bucket, &oi.name).await;
|
||||
if versioned {
|
||||
oi.replication_status = ReplicationStatusType::from(
|
||||
oi.user_defined
|
||||
.get("x-amz-bucket-replication-status")
|
||||
.unwrap_or(&"PENDING".to_string()),
|
||||
);
|
||||
debug!("apply status is: {:?}", oi.replication_status);
|
||||
self.heal_replication(&oi, _size_s).await;
|
||||
}
|
||||
|
||||
done();
|
||||
|
||||
if action.delete_all() {
|
||||
|
||||
@@ -1372,7 +1372,8 @@ impl StorageAPI for ECStore {
|
||||
}
|
||||
|
||||
if let Err(err) = self.peer_sys.make_bucket(bucket, opts).await {
|
||||
if !is_err_bucket_exists(&err.into()) {
|
||||
let err = err.into();
|
||||
if !is_err_bucket_exists(&err) {
|
||||
let _ = self
|
||||
.delete_bucket(
|
||||
bucket,
|
||||
@@ -1384,6 +1385,8 @@ impl StorageAPI for ECStore {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
let mut meta = BucketMetadata::new(bucket);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(unused_imports)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,6 +11,7 @@
|
||||
// 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.
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(unused_mut)]
|
||||
#![allow(unused_assignments)]
|
||||
|
||||
@@ -95,7 +95,6 @@ impl WarmBackendS3 {
|
||||
..Default::default()
|
||||
};
|
||||
let client = TransitionClient::new(&u.host().expect("err").to_string(), opts).await?;
|
||||
//client.set_appinfo(format!("s3-tier-{}", tier), ReleaseTag);
|
||||
|
||||
let client = Arc::new(client);
|
||||
let core = TransitionCore(Arc::clone(&client));
|
||||
|
||||
+196
-172
@@ -1,238 +1,262 @@
|
||||
# RustFS FileMeta
|
||||
[](https://rustfs.com)
|
||||
|
||||
A high-performance Rust implementation of xl-storage-format-v2, providing complete compatibility with S3-compatible metadata format while offering enhanced performance and safety.
|
||||
# RustFS FileMeta - File Metadata Management
|
||||
|
||||
## Overview
|
||||
<p align="center">
|
||||
<strong>High-performance file metadata management for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
This crate implements the XL (Erasure Coded) metadata format used for distributed object storage. It provides:
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
- **Full S3 Compatibility**: 100% compatible with xl.meta file format
|
||||
- **High Performance**: Optimized for speed with sub-microsecond parsing times
|
||||
- **Memory Safety**: Written in safe Rust with comprehensive error handling
|
||||
- **Comprehensive Testing**: Extensive test suite with real metadata validation
|
||||
- **Cross-Platform**: Supports multiple CPU architectures (x86_64, aarch64)
|
||||
---
|
||||
|
||||
## Features
|
||||
## 📖 Overview
|
||||
|
||||
### Core Functionality
|
||||
- ✅ XL v2 file format parsing and serialization
|
||||
- ✅ MessagePack-based metadata encoding/decoding
|
||||
- ✅ Version management with modification time sorting
|
||||
- ✅ Erasure coding information storage
|
||||
- ✅ Inline data support for small objects
|
||||
- ✅ CRC32 integrity verification using xxHash64
|
||||
- ✅ Delete marker handling
|
||||
- ✅ Legacy version support
|
||||
**RustFS FileMeta** is the metadata management module for the [RustFS](https://rustfs.com) distributed object storage system. It provides efficient storage, retrieval, and management of file metadata, supporting features like versioning, tagging, and extended attributes with high performance and reliability.
|
||||
|
||||
### Advanced Features
|
||||
- ✅ Signature calculation for version integrity
|
||||
- ✅ Metadata validation and compatibility checking
|
||||
- ✅ Version statistics and analytics
|
||||
- ✅ Async I/O support with tokio
|
||||
- ✅ Comprehensive error handling
|
||||
- ✅ Performance benchmarking
|
||||
> **Note:** This is a core submodule of RustFS that provides essential metadata management capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## Performance
|
||||
## ✨ Features
|
||||
|
||||
Based on our benchmarks:
|
||||
### 📝 Metadata Management
|
||||
- **File Information**: Complete file metadata including size, timestamps, and checksums
|
||||
- **Object Versioning**: Version-aware metadata management
|
||||
- **Extended Attributes**: Custom metadata and tagging support
|
||||
- **Inline Metadata**: Optimized storage for small metadata
|
||||
|
||||
| Operation | Time | Description |
|
||||
|-----------|------|-------------|
|
||||
| Parse Real xl.meta | ~255 ns | Parse authentic xl metadata |
|
||||
| Parse Complex xl.meta | ~1.1 µs | Parse multi-version metadata |
|
||||
| Serialize Real xl.meta | ~659 ns | Serialize to xl format |
|
||||
| Round-trip Real xl.meta | ~1.3 µs | Parse + serialize cycle |
|
||||
| Version Statistics | ~5.2 ns | Calculate version stats |
|
||||
| Integrity Validation | ~7.8 ns | Validate metadata integrity |
|
||||
### 🚀 Performance Features
|
||||
- **FlatBuffers Serialization**: Zero-copy metadata serialization
|
||||
- **Efficient Storage**: Optimized metadata storage layout
|
||||
- **Fast Lookups**: High-performance metadata queries
|
||||
- **Batch Operations**: Bulk metadata operations
|
||||
|
||||
## Usage
|
||||
### 🔧 Advanced Capabilities
|
||||
- **Schema Evolution**: Forward and backward compatible metadata schemas
|
||||
- **Compression**: Metadata compression for space efficiency
|
||||
- **Validation**: Metadata integrity verification
|
||||
- **Migration**: Seamless metadata format migration
|
||||
|
||||
### Basic Usage
|
||||
## 📦 Installation
|
||||
|
||||
```rust
|
||||
use rustfs_filemeta::file_meta::FileMeta;
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
// Load metadata from bytes
|
||||
let metadata = FileMeta::load(&xl_meta_bytes)?;
|
||||
|
||||
// Access version information
|
||||
for version in &metadata.versions {
|
||||
println!("Version ID: {:?}", version.header.version_id);
|
||||
println!("Mod Time: {:?}", version.header.mod_time);
|
||||
}
|
||||
|
||||
// Serialize back to bytes
|
||||
let serialized = metadata.marshal_msg()?;
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-filemeta = "0.1.0"
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Metadata Operations
|
||||
|
||||
```rust
|
||||
use rustfs_filemeta::file_meta::FileMeta;
|
||||
use rustfs_filemeta::{FileInfo, XLMeta};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Load with validation
|
||||
let mut metadata = FileMeta::load(&xl_meta_bytes)?;
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create file metadata
|
||||
let mut file_info = FileInfo::new();
|
||||
file_info.name = "example.txt".to_string();
|
||||
file_info.size = 1024;
|
||||
file_info.mod_time = chrono::Utc::now();
|
||||
|
||||
// Validate integrity
|
||||
metadata.validate_integrity()?;
|
||||
// Add custom metadata
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert("author".to_string(), "john@example.com".to_string());
|
||||
user_defined.insert("department".to_string(), "engineering".to_string());
|
||||
file_info.user_defined = user_defined;
|
||||
|
||||
// Check xl format compatibility
|
||||
if metadata.is_compatible_with_meta() {
|
||||
println!("Compatible with xl format");
|
||||
}
|
||||
// Create XL metadata
|
||||
let xl_meta = XLMeta::new(file_info);
|
||||
|
||||
// Get version statistics
|
||||
let stats = metadata.get_version_stats();
|
||||
println!("Total versions: {}", stats.total_versions);
|
||||
println!("Object versions: {}", stats.object_versions);
|
||||
println!("Delete markers: {}", stats.delete_markers);
|
||||
```
|
||||
// Serialize metadata
|
||||
let serialized = xl_meta.serialize()?;
|
||||
|
||||
### Working with FileInfo
|
||||
// Deserialize metadata
|
||||
let deserialized = XLMeta::deserialize(&serialized)?;
|
||||
|
||||
```rust
|
||||
use rustfs_filemeta::fileinfo::FileInfo;
|
||||
use rustfs_filemeta::file_meta::FileMetaVersion;
|
||||
println!("File: {}, Size: {}", deserialized.file_info.name, deserialized.file_info.size);
|
||||
|
||||
// Convert FileInfo to metadata version
|
||||
let file_info = FileInfo::new("bucket", "object.txt");
|
||||
let meta_version = FileMetaVersion::from(file_info);
|
||||
|
||||
// Add version to metadata
|
||||
metadata.add_version(file_info)?;
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### FileMeta
|
||||
The main metadata container that holds all versions and inline data:
|
||||
|
||||
```rust
|
||||
pub struct FileMeta {
|
||||
pub versions: Vec<FileMetaShallowVersion>,
|
||||
pub data: InlineData,
|
||||
pub meta_ver: u8,
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### FileMetaVersion
|
||||
Represents a single object version:
|
||||
### Advanced Metadata Management
|
||||
|
||||
```rust
|
||||
pub struct FileMetaVersion {
|
||||
pub version_type: VersionType,
|
||||
pub object: Option<MetaObject>,
|
||||
pub delete_marker: Option<MetaDeleteMarker>,
|
||||
pub write_version: u64,
|
||||
use rustfs_filemeta::{XLMeta, FileInfo, VersionInfo};
|
||||
|
||||
async fn advanced_metadata_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create versioned metadata
|
||||
let mut xl_meta = XLMeta::new(FileInfo::default());
|
||||
|
||||
// Set version information
|
||||
xl_meta.set_version_info(VersionInfo {
|
||||
version_id: "v1.0.0".to_string(),
|
||||
is_latest: true,
|
||||
delete_marker: false,
|
||||
restore_ongoing: false,
|
||||
});
|
||||
|
||||
// Add checksums
|
||||
xl_meta.add_checksum("md5", "d41d8cd98f00b204e9800998ecf8427e");
|
||||
xl_meta.add_checksum("sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
|
||||
// Set object tags
|
||||
xl_meta.set_tags(vec![
|
||||
("Environment".to_string(), "Production".to_string()),
|
||||
("Owner".to_string(), "DataTeam".to_string()),
|
||||
]);
|
||||
|
||||
// Set retention information
|
||||
xl_meta.set_retention_info(
|
||||
chrono::Utc::now() + chrono::Duration::days(365),
|
||||
"GOVERNANCE".to_string(),
|
||||
);
|
||||
|
||||
// Validate metadata
|
||||
xl_meta.validate()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### MetaObject
|
||||
Contains object-specific metadata including erasure coding information:
|
||||
### Inline Metadata Operations
|
||||
|
||||
```rust
|
||||
pub struct MetaObject {
|
||||
pub version_id: Option<Uuid>,
|
||||
pub data_dir: Option<Uuid>,
|
||||
pub erasure_algorithm: ErasureAlgo,
|
||||
pub erasure_m: usize,
|
||||
pub erasure_n: usize,
|
||||
// ... additional fields
|
||||
use rustfs_filemeta::{InlineMetadata, MetadataSize};
|
||||
|
||||
fn inline_metadata_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create inline metadata for small files
|
||||
let mut inline_meta = InlineMetadata::new();
|
||||
|
||||
// Set basic properties
|
||||
inline_meta.set_content_type("text/plain");
|
||||
inline_meta.set_content_encoding("gzip");
|
||||
inline_meta.set_cache_control("max-age=3600");
|
||||
|
||||
// Add custom headers
|
||||
inline_meta.add_header("x-custom-field", "custom-value");
|
||||
inline_meta.add_header("x-app-version", "1.2.3");
|
||||
|
||||
// Check if metadata fits inline storage
|
||||
if inline_meta.size() <= MetadataSize::INLINE_THRESHOLD {
|
||||
println!("Metadata can be stored inline");
|
||||
} else {
|
||||
println!("Metadata requires separate storage");
|
||||
}
|
||||
|
||||
// Serialize for storage
|
||||
let bytes = inline_meta.to_bytes()?;
|
||||
|
||||
// Deserialize from storage
|
||||
let restored = InlineMetadata::from_bytes(&bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## File Format Compatibility
|
||||
## 🏗️ Architecture
|
||||
|
||||
This implementation is fully compatible with xl-storage-format-v2:
|
||||
### Metadata Storage Layout
|
||||
|
||||
- **Header Format**: XL2 v1 format with proper version checking
|
||||
- **Serialization**: MessagePack encoding identical to standard format
|
||||
- **Checksums**: xxHash64-based CRC validation
|
||||
- **Version Types**: Support for Object, Delete, and Legacy versions
|
||||
- **Inline Data**: Compatible inline data storage for small objects
|
||||
```
|
||||
FileMeta Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Metadata API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ XL Metadata │ Inline Metadata │ Version Info │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ FlatBuffers Serialization │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Compression │ Validation │ Migration │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Storage Backend Integration │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Testing
|
||||
### Metadata Types
|
||||
|
||||
The crate includes comprehensive tests with real xl metadata:
|
||||
| Type | Use Case | Storage | Performance |
|
||||
|------|----------|---------|-------------|
|
||||
| XLMeta | Large objects with rich metadata | Separate file | High durability |
|
||||
| InlineMeta | Small objects with minimal metadata | Embedded | Fastest access |
|
||||
| VersionMeta | Object versioning information | Version-specific | Version-aware |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run benchmarks
|
||||
# Run serialization benchmarks
|
||||
cargo bench
|
||||
|
||||
# Run with coverage
|
||||
cargo test --features coverage
|
||||
# Test metadata validation
|
||||
cargo test validation
|
||||
|
||||
# Test schema migration
|
||||
cargo test migration
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
- ✅ Real xl.meta file compatibility
|
||||
- ✅ Complex multi-version scenarios
|
||||
- ✅ Error handling and recovery
|
||||
- ✅ Inline data processing
|
||||
- ✅ Signature calculation
|
||||
- ✅ Round-trip serialization
|
||||
- ✅ Performance benchmarks
|
||||
- ✅ Edge cases and boundary conditions
|
||||
## 🚀 Performance
|
||||
|
||||
## Architecture
|
||||
FileMeta is optimized for high-performance metadata operations:
|
||||
|
||||
The crate follows a modular design:
|
||||
- **Serialization**: Zero-copy FlatBuffers serialization
|
||||
- **Storage**: Compact binary format reduces I/O
|
||||
- **Caching**: Intelligent metadata caching
|
||||
- **Batch Operations**: Efficient bulk metadata processing
|
||||
|
||||
```
|
||||
src/
|
||||
├── file_meta.rs # Core metadata structures and logic
|
||||
├── file_meta_inline.rs # Inline data handling
|
||||
├── fileinfo.rs # File information structures
|
||||
├── test_data.rs # Test data generation
|
||||
└── lib.rs # Public API exports
|
||||
```
|
||||
## 📋 Requirements
|
||||
|
||||
## Error Handling
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Memory**: Minimal memory footprint
|
||||
- **Storage**: Compatible with RustFS storage backend
|
||||
|
||||
Comprehensive error handling with detailed error messages:
|
||||
## 🌍 Related Projects
|
||||
|
||||
```rust
|
||||
use rustfs_filemeta::error::Error;
|
||||
This module is part of the RustFS ecosystem:
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS ECStore](../ecstore) - Erasure coding storage engine
|
||||
- [RustFS Utils](../utils) - Utility functions
|
||||
- [RustFS Proto](../protos) - Protocol definitions
|
||||
|
||||
match FileMeta::load(&invalid_data) {
|
||||
Ok(metadata) => { /* process metadata */ },
|
||||
Err(Error::InvalidFormat(msg)) => {
|
||||
eprintln!("Invalid format: {}", msg);
|
||||
},
|
||||
Err(Error::CorruptedData(msg)) => {
|
||||
eprintln!("Corrupted data: {}", msg);
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Other error: {}", e);
|
||||
}
|
||||
}
|
||||
```
|
||||
## 📚 Documentation
|
||||
|
||||
## Dependencies
|
||||
For comprehensive documentation, visit:
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [FileMeta API Reference](https://docs.rustfs.com/filemeta/)
|
||||
|
||||
- `rmp` - MessagePack serialization
|
||||
- `uuid` - UUID handling
|
||||
- `time` - Date/time operations
|
||||
- `xxhash-rust` - Fast hashing
|
||||
- `tokio` - Async runtime (optional)
|
||||
- `criterion` - Benchmarking (dev dependency)
|
||||
## 🔗 Links
|
||||
|
||||
## Contributing
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Add tests for new functionality
|
||||
4. Ensure all tests pass
|
||||
5. Submit a pull request
|
||||
## 🤝 Contributing
|
||||
|
||||
## License
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
|
||||
## 📄 License
|
||||
|
||||
## Acknowledgments
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
- Original xl-storage-format-v2 implementation contributors
|
||||
- Rust community for excellent crates and tooling
|
||||
- Contributors and testers who helped improve this implementation
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ by the RustFS Team
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS IAM - Identity and Access Management
|
||||
|
||||
<p align="center">
|
||||
<strong>Enterprise-grade identity and access management for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS IAM** is the identity and access management module for the [RustFS](https://rustfs.com) distributed object storage system. It provides comprehensive authentication, authorization, and access control capabilities, ensuring secure and compliant access to storage resources.
|
||||
|
||||
> **Note:** This is a core submodule of RustFS and provides essential security and access control features for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔐 Authentication & Authorization
|
||||
|
||||
- **Multi-Factor Authentication**: Support for various authentication methods
|
||||
- **Access Key Management**: Secure generation and management of access keys
|
||||
- **JWT Token Support**: Stateless authentication with JWT tokens
|
||||
- **Session Management**: Secure session handling and token refresh
|
||||
|
||||
### 👥 User Management
|
||||
|
||||
- **User Accounts**: Complete user lifecycle management
|
||||
- **Service Accounts**: Automated service authentication
|
||||
- **Temporary Accounts**: Time-limited access credentials
|
||||
- **Group Management**: Organize users into groups for easier management
|
||||
|
||||
### 🛡️ Access Control
|
||||
|
||||
- **Role-Based Access Control (RBAC)**: Flexible role and permission system
|
||||
- **Policy-Based Access Control**: Fine-grained access policies
|
||||
- **Resource-Level Permissions**: Granular control over storage resources
|
||||
- **API-Level Authorization**: Secure API access control
|
||||
|
||||
### 🔑 Credential Management
|
||||
|
||||
- **Secure Key Generation**: Cryptographically secure key generation
|
||||
- **Key Rotation**: Automatic and manual key rotation capabilities
|
||||
- **Credential Validation**: Real-time credential verification
|
||||
- **Secret Management**: Secure storage and retrieval of secrets
|
||||
|
||||
### 🏢 Enterprise Features
|
||||
|
||||
- **LDAP Integration**: Enterprise directory service integration
|
||||
- **SSO Support**: Single Sign-On capabilities
|
||||
- **Audit Logging**: Comprehensive access audit trails
|
||||
- **Compliance Features**: Meet regulatory compliance requirements
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### IAM System Architecture
|
||||
|
||||
```
|
||||
IAM Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ IAM API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Authentication │ Authorization │ User Management │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Policy Engine Integration │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Credential Store │ Cache Layer │ Token Manager │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Storage Backend Integration │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Security Model
|
||||
|
||||
| Component | Description | Security Level |
|
||||
|-----------|-------------|----------------|
|
||||
| Access Keys | API authentication credentials | High |
|
||||
| JWT Tokens | Stateless authentication tokens | High |
|
||||
| Session Management | User session handling | Medium |
|
||||
| Policy Enforcement | Access control policies | Critical |
|
||||
| Audit Logging | Security event tracking | High |
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-iam = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic IAM Setup
|
||||
|
||||
```rust
|
||||
use rustfs_iam::{init_iam_sys, get};
|
||||
use rustfs_ecstore::ECStore;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize with ECStore backend
|
||||
let ecstore = Arc::new(ECStore::new("/path/to/storage").await?);
|
||||
|
||||
// Initialize IAM system
|
||||
init_iam_sys(ecstore).await?;
|
||||
|
||||
// Get IAM system instance
|
||||
let iam = get()?;
|
||||
|
||||
println!("IAM system initialized successfully");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### User Management
|
||||
|
||||
```rust
|
||||
use rustfs_iam::{get, manager::UserInfo};
|
||||
|
||||
async fn user_management_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let iam = get()?;
|
||||
|
||||
// Create a new user
|
||||
let user_info = UserInfo {
|
||||
access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
|
||||
secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
|
||||
status: "enabled".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
iam.create_user("john-doe", user_info).await?;
|
||||
|
||||
// List users
|
||||
let users = iam.list_users().await?;
|
||||
for user in users {
|
||||
println!("User: {}, Status: {}", user.name, user.status);
|
||||
}
|
||||
|
||||
// Update user status
|
||||
iam.set_user_status("john-doe", "disabled").await?;
|
||||
|
||||
// Delete user
|
||||
iam.delete_user("john-doe").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Group Management
|
||||
|
||||
```rust
|
||||
use rustfs_iam::{get, manager::GroupInfo};
|
||||
|
||||
async fn group_management_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let iam = get()?;
|
||||
|
||||
// Create a group
|
||||
let group_info = GroupInfo {
|
||||
name: "developers".to_string(),
|
||||
members: vec!["john-doe".to_string(), "jane-smith".to_string()],
|
||||
policies: vec!["read-only-policy".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
iam.create_group(group_info).await?;
|
||||
|
||||
// Add user to group
|
||||
iam.add_user_to_group("alice", "developers").await?;
|
||||
|
||||
// Remove user from group
|
||||
iam.remove_user_from_group("alice", "developers").await?;
|
||||
|
||||
// List groups
|
||||
let groups = iam.list_groups().await?;
|
||||
for group in groups {
|
||||
println!("Group: {}, Members: {}", group.name, group.members.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Policy Management
|
||||
|
||||
```rust
|
||||
use rustfs_iam::{get, manager::PolicyDocument};
|
||||
|
||||
async fn policy_management_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let iam = get()?;
|
||||
|
||||
// Create a policy
|
||||
let policy_doc = PolicyDocument {
|
||||
version: "2012-10-17".to_string(),
|
||||
statement: vec![
|
||||
Statement {
|
||||
effect: "Allow".to_string(),
|
||||
action: vec!["s3:GetObject".to_string()],
|
||||
resource: vec!["arn:aws:s3:::my-bucket/*".to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
iam.create_policy("read-only-policy", policy_doc).await?;
|
||||
|
||||
// Attach policy to user
|
||||
iam.attach_user_policy("john-doe", "read-only-policy").await?;
|
||||
|
||||
// Detach policy from user
|
||||
iam.detach_user_policy("john-doe", "read-only-policy").await?;
|
||||
|
||||
// List policies
|
||||
let policies = iam.list_policies().await?;
|
||||
for policy in policies {
|
||||
println!("Policy: {}", policy.name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Service Account Management
|
||||
|
||||
```rust
|
||||
use rustfs_iam::{get, manager::ServiceAccountInfo};
|
||||
|
||||
async fn service_account_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let iam = get()?;
|
||||
|
||||
// Create service account
|
||||
let service_account = ServiceAccountInfo {
|
||||
name: "backup-service".to_string(),
|
||||
description: "Automated backup service".to_string(),
|
||||
policies: vec!["backup-policy".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
iam.create_service_account(service_account).await?;
|
||||
|
||||
// Generate credentials for service account
|
||||
let credentials = iam.generate_service_account_credentials("backup-service").await?;
|
||||
println!("Service Account Credentials: {:?}", credentials);
|
||||
|
||||
// Rotate service account credentials
|
||||
iam.rotate_service_account_credentials("backup-service").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication and Authorization
|
||||
|
||||
```rust
|
||||
use rustfs_iam::{get, auth::Credentials};
|
||||
|
||||
async fn auth_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let iam = get()?;
|
||||
|
||||
// Authenticate user
|
||||
let credentials = Credentials {
|
||||
access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
|
||||
secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
|
||||
session_token: None,
|
||||
};
|
||||
|
||||
let auth_result = iam.authenticate(&credentials).await?;
|
||||
println!("Authentication successful: {}", auth_result.user_name);
|
||||
|
||||
// Check authorization
|
||||
let authorized = iam.is_authorized(
|
||||
&auth_result.user_name,
|
||||
"s3:GetObject",
|
||||
"arn:aws:s3:::my-bucket/file.txt"
|
||||
).await?;
|
||||
|
||||
if authorized {
|
||||
println!("User is authorized to access the resource");
|
||||
} else {
|
||||
println!("User is not authorized to access the resource");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Temporary Credentials
|
||||
|
||||
```rust
|
||||
use rustfs_iam::{get, manager::TemporaryCredentials};
|
||||
use std::time::Duration;
|
||||
|
||||
async fn temp_credentials_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let iam = get()?;
|
||||
|
||||
// Create temporary credentials
|
||||
let temp_creds = iam.create_temporary_credentials(
|
||||
"john-doe",
|
||||
Duration::from_secs(3600), // 1 hour
|
||||
Some("read-only-policy".to_string())
|
||||
).await?;
|
||||
|
||||
println!("Temporary Access Key: {}", temp_creds.access_key);
|
||||
println!("Expires at: {}", temp_creds.expiration);
|
||||
|
||||
// Validate temporary credentials
|
||||
let is_valid = iam.validate_temporary_credentials(&temp_creds.access_key).await?;
|
||||
println!("Temporary credentials valid: {}", is_valid);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run tests with specific features
|
||||
cargo test --features "ldap,sso"
|
||||
|
||||
# Run integration tests
|
||||
cargo test --test integration
|
||||
|
||||
# Run authentication tests
|
||||
cargo test auth
|
||||
|
||||
# Run authorization tests
|
||||
cargo test authz
|
||||
```
|
||||
|
||||
## 🔒 Security Best Practices
|
||||
|
||||
### Key Management
|
||||
|
||||
- Rotate access keys regularly
|
||||
- Use strong, randomly generated keys
|
||||
- Store keys securely using environment variables or secret management systems
|
||||
- Implement key rotation policies
|
||||
|
||||
### Access Control
|
||||
|
||||
- Follow the principle of least privilege
|
||||
- Use groups for easier permission management
|
||||
- Regularly audit user permissions
|
||||
- Implement resource-based policies
|
||||
|
||||
### Monitoring and Auditing
|
||||
|
||||
- Enable comprehensive audit logging
|
||||
- Monitor failed authentication attempts
|
||||
- Set up alerts for suspicious activities
|
||||
- Regular security reviews
|
||||
|
||||
## 📊 Performance Considerations
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
- **User Cache**: Cache user information for faster lookups
|
||||
- **Policy Cache**: Cache policy documents to reduce latency
|
||||
- **Token Cache**: Cache JWT tokens for stateless authentication
|
||||
- **Permission Cache**: Cache authorization decisions
|
||||
|
||||
### Scalability
|
||||
|
||||
- **Distributed Cache**: Use distributed caching for multi-node deployments
|
||||
- **Database Optimization**: Optimize database queries for user/group lookups
|
||||
- **Connection Pooling**: Use connection pooling for database connections
|
||||
- **Async Operations**: Leverage async programming for better throughput
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```toml
|
||||
[iam]
|
||||
# Authentication settings
|
||||
jwt_secret = "your-jwt-secret-key"
|
||||
jwt_expiration = "24h"
|
||||
session_timeout = "30m"
|
||||
|
||||
# Password policy
|
||||
min_password_length = 8
|
||||
require_special_chars = true
|
||||
require_numbers = true
|
||||
require_uppercase = true
|
||||
|
||||
# Account lockout
|
||||
max_login_attempts = 5
|
||||
lockout_duration = "15m"
|
||||
|
||||
# Audit settings
|
||||
audit_enabled = true
|
||||
audit_log_path = "/var/log/rustfs/iam-audit.log"
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```rust
|
||||
use rustfs_iam::config::IamConfig;
|
||||
|
||||
let config = IamConfig {
|
||||
// Authentication settings
|
||||
jwt_secret: "your-secure-jwt-secret".to_string(),
|
||||
jwt_expiration_hours: 24,
|
||||
session_timeout_minutes: 30,
|
||||
|
||||
// Security settings
|
||||
password_policy: PasswordPolicy {
|
||||
min_length: 8,
|
||||
require_special_chars: true,
|
||||
require_numbers: true,
|
||||
require_uppercase: true,
|
||||
max_age_days: 90,
|
||||
},
|
||||
|
||||
// Rate limiting
|
||||
rate_limit: RateLimit {
|
||||
max_requests_per_minute: 100,
|
||||
burst_size: 10,
|
||||
},
|
||||
|
||||
// Audit settings
|
||||
audit_enabled: true,
|
||||
audit_log_level: "info".to_string(),
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
```
|
||||
|
||||
## 🤝 Integration with RustFS
|
||||
|
||||
IAM integrates seamlessly with other RustFS components:
|
||||
|
||||
- **ECStore**: Provides user and policy storage backend
|
||||
- **Policy Engine**: Implements fine-grained access control
|
||||
- **Crypto Module**: Handles secure key generation and JWT operations
|
||||
- **API Server**: Provides authentication and authorization for S3 API
|
||||
- **Admin Interface**: Manages users, groups, and policies
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Database**: Compatible with RustFS storage backend
|
||||
- **Memory**: Minimum 2GB RAM for caching
|
||||
- **Network**: Secure connections for authentication
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Authentication Failures**:
|
||||
- Check access key and secret key validity
|
||||
- Verify user account status (enabled/disabled)
|
||||
- Check for account lockout due to failed attempts
|
||||
|
||||
2. **Authorization Errors**:
|
||||
- Verify user has required permissions
|
||||
- Check policy attachments (user/group policies)
|
||||
- Validate resource ARN format
|
||||
|
||||
3. **Performance Issues**:
|
||||
- Monitor cache hit rates
|
||||
- Check database connection pool utilization
|
||||
- Verify JWT token size and complexity
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check IAM system status
|
||||
rustfs-cli iam status
|
||||
|
||||
# List all users
|
||||
rustfs-cli iam list-users
|
||||
|
||||
# Validate user credentials
|
||||
rustfs-cli iam validate-credentials --access-key <key>
|
||||
|
||||
# Test policy evaluation
|
||||
rustfs-cli iam test-policy --user <user> --action <action> --resource <resource>
|
||||
```
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS ECStore](../ecstore) - Erasure coding storage engine
|
||||
- [RustFS Policy](../policy) - Policy engine for access control
|
||||
- [RustFS Crypto](../crypto) - Cryptographic operations
|
||||
- [RustFS MadAdmin](../madmin) - Administrative interface
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [IAM API Reference](https://docs.rustfs.com/iam/)
|
||||
- [Security Guide](https://docs.rustfs.com/security/)
|
||||
- [Authentication Guide](https://docs.rustfs.com/auth/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details on:
|
||||
|
||||
- Security-first development practices
|
||||
- IAM system architecture guidelines
|
||||
- Authentication and authorization patterns
|
||||
- Testing procedures for security features
|
||||
- Documentation standards for security APIs
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/rustfs/rustfs.git
|
||||
cd rustfs
|
||||
|
||||
# Navigate to IAM module
|
||||
cd crates/iam
|
||||
|
||||
# Install dependencies
|
||||
cargo build
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Run security tests
|
||||
cargo test security
|
||||
|
||||
# Format code
|
||||
cargo fmt
|
||||
|
||||
# Run linter
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
## 💬 Getting Help
|
||||
|
||||
- **Documentation**: [docs.rustfs.com](https://docs.rustfs.com)
|
||||
- **Issues**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
- **Security**: Report security issues to <security@rustfs.com>
|
||||
|
||||
## 📞 Contact
|
||||
|
||||
- **Bugs**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||
- **Business**: <hello@rustfs.com>
|
||||
- **Jobs**: <jobs@rustfs.com>
|
||||
- **General Discussion**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
This module is maintained by the RustFS security team and community contributors. Special thanks to all who have contributed to making RustFS secure and compliant.
|
||||
|
||||
<a href="https://github.com/rustfs/rustfs/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=rustfs/rustfs" />
|
||||
</a>
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🔐 by the RustFS Security Team
|
||||
</p>
|
||||
@@ -0,0 +1,392 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Lock - Distributed Locking
|
||||
|
||||
<p align="center">
|
||||
<strong>Distributed locking and synchronization for RustFS object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Lock** provides distributed locking and synchronization primitives for the [RustFS](https://rustfs.com) distributed object storage system. It ensures data consistency and prevents race conditions in multi-node environments through various locking mechanisms and coordination protocols.
|
||||
|
||||
> **Note:** This is a core submodule of RustFS that provides essential distributed locking capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔒 Distributed Locking
|
||||
|
||||
- **Exclusive Locks**: Mutual exclusion across cluster nodes
|
||||
- **Shared Locks**: Reader-writer lock semantics
|
||||
- **Timeout Support**: Configurable lock timeouts and expiration
|
||||
- **Deadlock Prevention**: Automatic deadlock detection and resolution
|
||||
|
||||
### 🔄 Synchronization Primitives
|
||||
|
||||
- **Distributed Mutex**: Cross-node mutual exclusion
|
||||
- **Distributed Semaphore**: Resource counting across nodes
|
||||
- **Distributed Barrier**: Coordination point for multiple nodes
|
||||
- **Distributed Condition Variables**: Wait/notify across nodes
|
||||
|
||||
### 🛡️ Consistency Guarantees
|
||||
|
||||
- **Linearizable Operations**: Strong consistency guarantees
|
||||
- **Fault Tolerance**: Automatic recovery from node failures
|
||||
- **Network Partition Handling**: CAP theorem aware implementations
|
||||
- **Consensus Integration**: Raft-based consensus for critical locks
|
||||
|
||||
### 🚀 Performance Features
|
||||
|
||||
- **Lock Coalescing**: Efficient batching of lock operations
|
||||
- **Adaptive Timeouts**: Dynamic timeout adjustment
|
||||
- **Lock Hierarchy**: Hierarchical locking for better scalability
|
||||
- **Optimistic Locking**: Reduced contention through optimistic approaches
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-lock = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Distributed Lock
|
||||
|
||||
```rust
|
||||
use rustfs_lock::{DistributedLock, LockManager, LockOptions};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create lock manager
|
||||
let lock_manager = LockManager::new("cluster-endpoint").await?;
|
||||
|
||||
// Acquire distributed lock
|
||||
let lock_options = LockOptions {
|
||||
timeout: Duration::from_secs(30),
|
||||
auto_renew: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lock = lock_manager.acquire_lock("resource-key", lock_options).await?;
|
||||
|
||||
// Critical section
|
||||
{
|
||||
println!("Lock acquired, performing critical operations...");
|
||||
// Your critical code here
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
|
||||
// Release lock
|
||||
lock.release().await?;
|
||||
println!("Lock released");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Distributed Mutex
|
||||
|
||||
```rust
|
||||
use rustfs_lock::{DistributedMutex, LockManager};
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn distributed_mutex_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let lock_manager = Arc::new(LockManager::new("cluster-endpoint").await?);
|
||||
|
||||
// Create distributed mutex
|
||||
let mutex = DistributedMutex::new(lock_manager.clone(), "shared-resource");
|
||||
|
||||
// Spawn multiple tasks
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..5 {
|
||||
let mutex = mutex.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let _guard = mutex.lock().await.unwrap();
|
||||
println!("Task {} acquired mutex", i);
|
||||
|
||||
// Simulate work
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
println!("Task {} releasing mutex", i);
|
||||
// Guard is automatically released when dropped
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
for handle in handles {
|
||||
handle.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Distributed Semaphore
|
||||
|
||||
```rust
|
||||
use rustfs_lock::{DistributedSemaphore, LockManager};
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn distributed_semaphore_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let lock_manager = Arc::new(LockManager::new("cluster-endpoint").await?);
|
||||
|
||||
// Create distributed semaphore with 3 permits
|
||||
let semaphore = DistributedSemaphore::new(
|
||||
lock_manager.clone(),
|
||||
"resource-pool",
|
||||
3
|
||||
);
|
||||
|
||||
// Spawn multiple tasks
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let semaphore = semaphore.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = semaphore.acquire().await.unwrap();
|
||||
println!("Task {} acquired permit", i);
|
||||
|
||||
// Simulate work
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
println!("Task {} releasing permit", i);
|
||||
// Permit is automatically released when dropped
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
for handle in handles {
|
||||
handle.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Distributed Barrier
|
||||
|
||||
```rust
|
||||
use rustfs_lock::{DistributedBarrier, LockManager};
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn distributed_barrier_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let lock_manager = Arc::new(LockManager::new("cluster-endpoint").await?);
|
||||
|
||||
// Create distributed barrier for 3 participants
|
||||
let barrier = DistributedBarrier::new(
|
||||
lock_manager.clone(),
|
||||
"sync-point",
|
||||
3
|
||||
);
|
||||
|
||||
// Spawn multiple tasks
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..3 {
|
||||
let barrier = barrier.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
println!("Task {} doing work...", i);
|
||||
|
||||
// Simulate different work durations
|
||||
tokio::time::sleep(Duration::from_secs(i + 1)).await;
|
||||
|
||||
println!("Task {} waiting at barrier", i);
|
||||
barrier.wait().await.unwrap();
|
||||
|
||||
println!("Task {} passed barrier", i);
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
for handle in handles {
|
||||
handle.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Lock with Automatic Renewal
|
||||
|
||||
```rust
|
||||
use rustfs_lock::{DistributedLock, LockManager, LockOptions};
|
||||
use std::time::Duration;
|
||||
|
||||
async fn auto_renewal_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let lock_manager = LockManager::new("cluster-endpoint").await?;
|
||||
|
||||
let lock_options = LockOptions {
|
||||
timeout: Duration::from_secs(10),
|
||||
auto_renew: true,
|
||||
renew_interval: Duration::from_secs(3),
|
||||
max_renewals: 5,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let lock = lock_manager.acquire_lock("long-running-task", lock_options).await?;
|
||||
|
||||
// Long-running operation
|
||||
for i in 0..20 {
|
||||
println!("Working on step {}", i);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
// Check if lock is still valid
|
||||
if !lock.is_valid().await? {
|
||||
println!("Lock lost, aborting operation");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lock.release().await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Hierarchical Locking
|
||||
|
||||
```rust
|
||||
use rustfs_lock::{LockManager, LockHierarchy, LockOptions};
|
||||
|
||||
async fn hierarchical_locking_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let lock_manager = LockManager::new("cluster-endpoint").await?;
|
||||
|
||||
// Create lock hierarchy
|
||||
let hierarchy = LockHierarchy::new(vec![
|
||||
"global-lock".to_string(),
|
||||
"bucket-lock".to_string(),
|
||||
"object-lock".to_string(),
|
||||
]);
|
||||
|
||||
// Acquire locks in hierarchy order
|
||||
let locks = lock_manager.acquire_hierarchical_locks(
|
||||
hierarchy,
|
||||
LockOptions::default()
|
||||
).await?;
|
||||
|
||||
// Critical section with hierarchical locks
|
||||
{
|
||||
println!("All hierarchical locks acquired");
|
||||
// Perform operations that require the full lock hierarchy
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
// Locks are automatically released in reverse order
|
||||
locks.release_all().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Lock Architecture
|
||||
|
||||
```
|
||||
Lock Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Lock API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Mutex │ Semaphore │ Barrier │ Condition │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Lock Manager │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Consensus │ Heartbeat │ Timeout │ Recovery │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Distributed Coordination │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Lock Types
|
||||
|
||||
| Type | Use Case | Guarantees |
|
||||
|------|----------|------------|
|
||||
| Exclusive | Critical sections | Mutual exclusion |
|
||||
| Shared | Reader-writer | Multiple readers |
|
||||
| Semaphore | Resource pooling | Counting semaphore |
|
||||
| Barrier | Synchronization | Coordination point |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test distributed locking
|
||||
cargo test distributed_lock
|
||||
|
||||
# Test synchronization primitives
|
||||
cargo test sync_primitives
|
||||
|
||||
# Test fault tolerance
|
||||
cargo test fault_tolerance
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Network**: Cluster connectivity required
|
||||
- **Consensus**: Raft consensus for critical operations
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Common](../common) - Common types and utilities
|
||||
- [RustFS Protos](../protos) - Protocol buffer definitions
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Lock API Reference](https://docs.rustfs.com/lock/)
|
||||
- [Distributed Systems Guide](https://docs.rustfs.com/distributed/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🔒 by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,351 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS MadAdmin - Administrative Interface
|
||||
|
||||
<p align="center">
|
||||
<strong>Administrative interface and management APIs for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS MadAdmin** provides comprehensive administrative interfaces and management APIs for the [RustFS](https://rustfs.com) distributed object storage system. It enables cluster management, monitoring, configuration, and administrative operations through both programmatic APIs and interactive interfaces.
|
||||
|
||||
> **Note:** This is a core submodule of RustFS that provides essential administrative capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🎛️ Cluster Management
|
||||
|
||||
- **Node Management**: Add, remove, and monitor cluster nodes
|
||||
- **Service Discovery**: Automatic service discovery and registration
|
||||
- **Load Balancing**: Distribute load across cluster nodes
|
||||
- **Health Monitoring**: Real-time cluster health monitoring
|
||||
|
||||
### 📊 System Monitoring
|
||||
|
||||
- **Performance Metrics**: CPU, memory, disk, and network metrics
|
||||
- **Storage Analytics**: Capacity planning and usage analytics
|
||||
- **Alert Management**: Configurable alerts and notifications
|
||||
- **Dashboard Interface**: Web-based monitoring dashboard
|
||||
|
||||
### ⚙️ Configuration Management
|
||||
|
||||
- **Dynamic Configuration**: Runtime configuration updates
|
||||
- **Policy Management**: Access control and bucket policies
|
||||
- **User Management**: User and group administration
|
||||
- **Backup Configuration**: Backup and restore settings
|
||||
|
||||
### 🔧 Administrative Operations
|
||||
|
||||
- **Data Migration**: Cross-cluster data migration
|
||||
- **Healing Operations**: Data integrity repair and healing
|
||||
- **Rebalancing**: Storage rebalancing operations
|
||||
- **Maintenance Mode**: Graceful maintenance operations
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-madmin = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Admin Client
|
||||
|
||||
```rust
|
||||
use rustfs_madmin::{AdminClient, AdminConfig, ServerInfo};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create admin client
|
||||
let config = AdminConfig {
|
||||
endpoint: "https://admin.rustfs.local:9001".to_string(),
|
||||
access_key: "admin".to_string(),
|
||||
secret_key: "password".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
};
|
||||
|
||||
let client = AdminClient::new(config).await?;
|
||||
|
||||
// Get server information
|
||||
let server_info = client.server_info().await?;
|
||||
println!("Server Version: {}", server_info.version);
|
||||
println!("Uptime: {}", server_info.uptime);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Cluster Management
|
||||
|
||||
```rust
|
||||
use rustfs_madmin::{AdminClient, AddServerRequest, RemoveServerRequest};
|
||||
|
||||
async fn cluster_management(client: &AdminClient) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// List cluster nodes
|
||||
let nodes = client.list_servers().await?;
|
||||
for node in nodes {
|
||||
println!("Node: {} - Status: {}", node.endpoint, node.state);
|
||||
}
|
||||
|
||||
// Add new server to cluster
|
||||
let add_request = AddServerRequest {
|
||||
endpoint: "https://new-node.rustfs.local:9000".to_string(),
|
||||
access_key: "node-key".to_string(),
|
||||
secret_key: "node-secret".to_string(),
|
||||
};
|
||||
|
||||
client.add_server(add_request).await?;
|
||||
println!("New server added successfully");
|
||||
|
||||
// Remove server from cluster
|
||||
let remove_request = RemoveServerRequest {
|
||||
endpoint: "https://old-node.rustfs.local:9000".to_string(),
|
||||
};
|
||||
|
||||
client.remove_server(remove_request).await?;
|
||||
println!("Server removed successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### System Monitoring
|
||||
|
||||
```rust
|
||||
use rustfs_madmin::{AdminClient, MetricsRequest, AlertConfig};
|
||||
|
||||
async fn monitoring_operations(client: &AdminClient) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Get system metrics
|
||||
let metrics = client.get_metrics(MetricsRequest::default()).await?;
|
||||
|
||||
println!("CPU Usage: {:.2}%", metrics.cpu_usage);
|
||||
println!("Memory Usage: {:.2}%", metrics.memory_usage);
|
||||
println!("Disk Usage: {:.2}%", metrics.disk_usage);
|
||||
|
||||
// Get storage information
|
||||
let storage_info = client.storage_info().await?;
|
||||
println!("Total Capacity: {} GB", storage_info.total_capacity / 1024 / 1024 / 1024);
|
||||
println!("Used Capacity: {} GB", storage_info.used_capacity / 1024 / 1024 / 1024);
|
||||
|
||||
// Configure alerts
|
||||
let alert_config = AlertConfig {
|
||||
name: "high-cpu-usage".to_string(),
|
||||
condition: "cpu_usage > 80".to_string(),
|
||||
notification_endpoint: "https://webhook.example.com/alerts".to_string(),
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
client.set_alert_config(alert_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### User and Policy Management
|
||||
|
||||
```rust
|
||||
use rustfs_madmin::{AdminClient, UserInfo, PolicyDocument};
|
||||
|
||||
async fn user_management(client: &AdminClient) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create user
|
||||
let user_info = UserInfo {
|
||||
access_key: "user123".to_string(),
|
||||
secret_key: "user-secret".to_string(),
|
||||
status: "enabled".to_string(),
|
||||
policy: Some("readwrite-policy".to_string()),
|
||||
};
|
||||
|
||||
client.add_user("new-user", user_info).await?;
|
||||
|
||||
// List users
|
||||
let users = client.list_users().await?;
|
||||
for (username, info) in users {
|
||||
println!("User: {} - Status: {}", username, info.status);
|
||||
}
|
||||
|
||||
// Set user policy
|
||||
let policy_doc = PolicyDocument {
|
||||
version: "2012-10-17".to_string(),
|
||||
statement: vec![/* policy statements */],
|
||||
};
|
||||
|
||||
client.set_user_policy("new-user", policy_doc).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Data Operations
|
||||
|
||||
```rust
|
||||
use rustfs_madmin::{AdminClient, HealRequest, RebalanceRequest};
|
||||
|
||||
async fn data_operations(client: &AdminClient) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Start healing operation
|
||||
let heal_request = HealRequest {
|
||||
bucket: Some("important-bucket".to_string()),
|
||||
prefix: Some("documents/".to_string()),
|
||||
recursive: true,
|
||||
dry_run: false,
|
||||
};
|
||||
|
||||
let heal_result = client.heal(heal_request).await?;
|
||||
println!("Healing started: {}", heal_result.heal_sequence);
|
||||
|
||||
// Check healing status
|
||||
let heal_status = client.heal_status(&heal_result.heal_sequence).await?;
|
||||
println!("Healing progress: {:.2}%", heal_status.progress);
|
||||
|
||||
// Start rebalancing
|
||||
let rebalance_request = RebalanceRequest {
|
||||
servers: vec![], // Empty means all servers
|
||||
};
|
||||
|
||||
client.start_rebalance(rebalance_request).await?;
|
||||
println!("Rebalancing started");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Management
|
||||
|
||||
```rust
|
||||
use rustfs_madmin::{AdminClient, ConfigUpdate, NotificationTarget};
|
||||
|
||||
async fn configuration_management(client: &AdminClient) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Get current configuration
|
||||
let config = client.get_config().await?;
|
||||
println!("Current config version: {}", config.version);
|
||||
|
||||
// Update configuration
|
||||
let config_update = ConfigUpdate {
|
||||
region: Some("us-west-2".to_string()),
|
||||
browser: Some(true),
|
||||
compression: Some(true),
|
||||
// ... other config fields
|
||||
};
|
||||
|
||||
client.set_config(config_update).await?;
|
||||
|
||||
// Configure notification targets
|
||||
let notification_target = NotificationTarget {
|
||||
arn: "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
|
||||
target_type: "webhook".to_string(),
|
||||
endpoint: "https://webhook.example.com/notifications".to_string(),
|
||||
};
|
||||
|
||||
client.set_notification_target("bucket1", notification_target).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### MadAdmin Architecture
|
||||
|
||||
```
|
||||
MadAdmin Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Admin API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Cluster Mgmt │ Monitoring │ User Management │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Data Ops │ Config Mgmt │ Notification │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ HTTP/gRPC Client Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Storage System Integration │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Administrative Operations
|
||||
|
||||
| Category | Operations | Description |
|
||||
|----------|------------|-------------|
|
||||
| Cluster | Add/Remove nodes, Health checks | Cluster management |
|
||||
| Monitoring | Metrics, Alerts, Dashboard | System monitoring |
|
||||
| Data | Healing, Rebalancing, Migration | Data operations |
|
||||
| Config | Settings, Policies, Notifications | Configuration |
|
||||
| Users | Authentication, Authorization | User management |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test admin operations
|
||||
cargo test admin_ops
|
||||
|
||||
# Test cluster management
|
||||
cargo test cluster
|
||||
|
||||
# Test monitoring
|
||||
cargo test monitoring
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Network**: Administrative access to RustFS cluster
|
||||
- **Permissions**: Administrative credentials required
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS IAM](../iam) - Identity and access management
|
||||
- [RustFS Policy](../policy) - Policy engine
|
||||
- [RustFS Common](../common) - Common types and utilities
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [MadAdmin API Reference](https://docs.rustfs.com/madmin/)
|
||||
- [Administrative Guide](https://docs.rustfs.com/admin/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🎛️ by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,415 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Notify - Event Notification System
|
||||
|
||||
<p align="center">
|
||||
<strong>Real-time event notification system for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Notify** is the event notification system for the [RustFS](https://rustfs.com) distributed object storage platform. It provides real-time event publishing and delivery to various targets including webhooks, MQTT brokers, and message queues, enabling seamless integration with external systems and workflows.
|
||||
|
||||
> **Note:** This is a core submodule of RustFS that provides essential event notification capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 📡 Event Publishing
|
||||
|
||||
- **Real-time Events**: Instant notification of storage events
|
||||
- **Event Filtering**: Advanced filtering based on object patterns and event types
|
||||
- **Reliable Delivery**: Guaranteed delivery with retry mechanisms
|
||||
- **Batch Processing**: Efficient batch event delivery
|
||||
|
||||
### 🎯 Multiple Targets
|
||||
|
||||
- **Webhooks**: HTTP/HTTPS webhook notifications
|
||||
- **MQTT**: MQTT broker integration for IoT scenarios
|
||||
- **Message Queues**: Integration with popular message queue systems
|
||||
- **Custom Targets**: Extensible target system for custom integrations
|
||||
|
||||
### 🔧 Advanced Features
|
||||
|
||||
- **Event Transformation**: Custom event payload transformation
|
||||
- **Pattern Matching**: Flexible pattern-based event filtering
|
||||
- **Rate Limiting**: Configurable rate limiting for targets
|
||||
- **Dead Letter Queue**: Failed event handling and recovery
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-notify = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Event Notification
|
||||
|
||||
```rust
|
||||
use rustfs_notify::{Event, EventType, NotificationTarget, NotifySystem};
|
||||
use rustfs_notify::target::WebhookTarget;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create notification system
|
||||
let notify_system = NotifySystem::new().await?;
|
||||
|
||||
// Create webhook target
|
||||
let webhook = WebhookTarget::new(
|
||||
"webhook-1",
|
||||
"https://api.example.com/webhook",
|
||||
vec![EventType::ObjectCreated, EventType::ObjectRemoved],
|
||||
);
|
||||
|
||||
// Add target to notification system
|
||||
notify_system.add_target(Box::new(webhook)).await?;
|
||||
|
||||
// Create and send event
|
||||
let event = Event::new(
|
||||
EventType::ObjectCreated,
|
||||
"my-bucket",
|
||||
"path/to/object.txt",
|
||||
"user123",
|
||||
);
|
||||
|
||||
notify_system.publish_event(event).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Event Configuration
|
||||
|
||||
```rust
|
||||
use rustfs_notify::{Event, EventType, NotificationConfig};
|
||||
use rustfs_notify::target::{WebhookTarget, MqttTarget};
|
||||
use rustfs_notify::filter::{EventFilter, PatternRule};
|
||||
|
||||
async fn setup_advanced_notifications() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let notify_system = NotifySystem::new().await?;
|
||||
|
||||
// Create webhook with custom configuration
|
||||
let webhook_config = NotificationConfig {
|
||||
retry_attempts: 3,
|
||||
retry_delay: std::time::Duration::from_secs(5),
|
||||
timeout: std::time::Duration::from_secs(30),
|
||||
rate_limit: Some(100), // 100 events per minute
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let webhook = WebhookTarget::builder()
|
||||
.id("production-webhook")
|
||||
.url("https://api.example.com/events")
|
||||
.events(vec![EventType::ObjectCreated, EventType::ObjectRemoved])
|
||||
.config(webhook_config)
|
||||
.headers(vec![
|
||||
("Authorization".to_string(), "Bearer token123".to_string()),
|
||||
("Content-Type".to_string(), "application/json".to_string()),
|
||||
])
|
||||
.build()?;
|
||||
|
||||
// Create MQTT target
|
||||
let mqtt_target = MqttTarget::builder()
|
||||
.id("iot-mqtt")
|
||||
.broker_url("mqtt://broker.example.com:1883")
|
||||
.topic("storage/events")
|
||||
.qos(1)
|
||||
.events(vec![EventType::ObjectCreated])
|
||||
.build()?;
|
||||
|
||||
// Add targets
|
||||
notify_system.add_target(Box::new(webhook)).await?;
|
||||
notify_system.add_target(Box::new(mqtt_target)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Event Filtering and Pattern Matching
|
||||
|
||||
```rust
|
||||
use rustfs_notify::filter::{EventFilter, PatternRule, ConditionRule};
|
||||
|
||||
fn setup_event_filters() -> Result<EventFilter, Box<dyn std::error::Error>> {
|
||||
let filter = EventFilter::builder()
|
||||
// Only images and documents
|
||||
.pattern_rule(PatternRule::new(
|
||||
"suffix",
|
||||
vec!["*.jpg", "*.png", "*.pdf", "*.doc"]
|
||||
))
|
||||
// Exclude temporary files
|
||||
.pattern_rule(PatternRule::new(
|
||||
"exclude",
|
||||
vec!["*/tmp/*", "*.tmp"]
|
||||
))
|
||||
// Only files larger than 1MB
|
||||
.condition_rule(ConditionRule::new(
|
||||
"object_size",
|
||||
">",
|
||||
1024 * 1024
|
||||
))
|
||||
// Only from specific buckets
|
||||
.bucket_filter(vec!["important-bucket", "backup-bucket"])
|
||||
.build()?;
|
||||
|
||||
Ok(filter)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Target Implementation
|
||||
|
||||
```rust
|
||||
use rustfs_notify::{Event, NotificationTarget, TargetResult};
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub struct SlackTarget {
|
||||
id: String,
|
||||
webhook_url: String,
|
||||
channel: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NotificationTarget for SlackTarget {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
async fn deliver_event(&self, event: &Event) -> TargetResult<()> {
|
||||
let message = format!(
|
||||
"🔔 Storage Event: {} in bucket `{}` - object `{}`",
|
||||
event.event_type,
|
||||
event.bucket_name,
|
||||
event.object_name
|
||||
);
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"text": message,
|
||||
"channel": self.channel,
|
||||
});
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&self.webhook_url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Slack delivery failed: {}", response.status()).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_event_type(&self, event_type: &EventType) -> bool {
|
||||
// Support all event types
|
||||
true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Transformation
|
||||
|
||||
```rust
|
||||
use rustfs_notify::{Event, EventTransformer};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub struct CustomEventTransformer;
|
||||
|
||||
impl EventTransformer for CustomEventTransformer {
|
||||
fn transform(&self, event: &Event) -> Value {
|
||||
json!({
|
||||
"eventVersion": "2.1",
|
||||
"eventSource": "rustfs:s3",
|
||||
"eventTime": event.timestamp.to_rfc3339(),
|
||||
"eventName": event.event_type.to_string(),
|
||||
"s3": {
|
||||
"bucket": {
|
||||
"name": event.bucket_name,
|
||||
"arn": format!("arn:aws:s3:::{}", event.bucket_name)
|
||||
},
|
||||
"object": {
|
||||
"key": event.object_name,
|
||||
"size": event.object_size.unwrap_or(0),
|
||||
"eTag": event.etag.as_ref().unwrap_or(&"".to_string()),
|
||||
}
|
||||
},
|
||||
"userIdentity": {
|
||||
"principalId": event.user_identity
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Notification System Architecture
|
||||
|
||||
```
|
||||
Notify Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Event Publisher │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Event Filter │ Event Queue │ Event Transformer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Target Manager │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Webhook Target │ MQTT Target │ Custom Targets │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Delivery Engine │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Supported Event Types
|
||||
|
||||
| Event Type | Description | Triggers |
|
||||
|-----------|-------------|----------|
|
||||
| `ObjectCreated` | Object creation events | PUT, POST, COPY |
|
||||
| `ObjectRemoved` | Object deletion events | DELETE |
|
||||
| `ObjectAccessed` | Object access events | GET, HEAD |
|
||||
| `ObjectRestore` | Object restoration events | Restore operations |
|
||||
| `BucketCreated` | Bucket creation events | CreateBucket |
|
||||
| `BucketRemoved` | Bucket deletion events | DeleteBucket |
|
||||
|
||||
### Target Types
|
||||
|
||||
| Target | Protocol | Use Case | Reliability |
|
||||
|--------|----------|----------|------------|
|
||||
| Webhook | HTTP/HTTPS | Web applications, APIs | High |
|
||||
| MQTT | MQTT | IoT devices, real-time systems | Medium |
|
||||
| Message Queue | AMQP, Redis | Microservices, async processing | High |
|
||||
| Custom | Any | Specialized integrations | Configurable |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run integration tests
|
||||
cargo test --test integration
|
||||
|
||||
# Test webhook delivery
|
||||
cargo test webhook
|
||||
|
||||
# Test MQTT integration
|
||||
cargo test mqtt
|
||||
|
||||
# Run with coverage
|
||||
cargo test --features test-coverage
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```toml
|
||||
[notify]
|
||||
# Global settings
|
||||
enabled = true
|
||||
max_concurrent_deliveries = 100
|
||||
default_retry_attempts = 3
|
||||
default_timeout = "30s"
|
||||
|
||||
# Queue settings
|
||||
event_queue_size = 10000
|
||||
batch_size = 100
|
||||
batch_timeout = "5s"
|
||||
|
||||
# Dead letter queue
|
||||
dlq_enabled = true
|
||||
dlq_max_size = 1000
|
||||
```
|
||||
|
||||
### Target Configuration
|
||||
|
||||
```toml
|
||||
[[notify.targets]]
|
||||
type = "webhook"
|
||||
id = "primary-webhook"
|
||||
url = "https://api.example.com/webhook"
|
||||
events = ["ObjectCreated", "ObjectRemoved"]
|
||||
retry_attempts = 5
|
||||
timeout = "30s"
|
||||
|
||||
[[notify.targets]]
|
||||
type = "mqtt"
|
||||
id = "iot-broker"
|
||||
broker_url = "mqtt://broker.example.com:1883"
|
||||
topic = "storage/events"
|
||||
qos = 1
|
||||
events = ["ObjectCreated"]
|
||||
```
|
||||
|
||||
## 🚀 Performance
|
||||
|
||||
The notification system is designed for high-throughput scenarios:
|
||||
|
||||
- **Async Processing**: Non-blocking event delivery
|
||||
- **Batch Delivery**: Efficient batch processing for high-volume events
|
||||
- **Connection Pooling**: Reused connections for better performance
|
||||
- **Rate Limiting**: Configurable rate limiting to prevent overwhelming targets
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Network**: Outbound connectivity for target delivery
|
||||
- **Memory**: Scales with event queue size
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS ECStore](../ecstore) - Erasure coding storage engine
|
||||
- [RustFS Config](../config) - Configuration management
|
||||
- [RustFS Utils](../utils) - Utility functions
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Notify API Reference](https://docs.rustfs.com/notify/)
|
||||
- [Event Configuration Guide](https://docs.rustfs.com/events/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 📡 by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,473 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Obs - Observability & Monitoring
|
||||
|
||||
<p align="center">
|
||||
<strong>Comprehensive observability and monitoring solution for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Obs** provides comprehensive observability and monitoring capabilities for the [RustFS](https://rustfs.com) distributed object storage system. It includes metrics collection, distributed tracing, logging, alerting, and performance monitoring to ensure optimal system operation and troubleshooting.
|
||||
|
||||
> **Note:** This is a critical operational submodule of RustFS that provides essential observability capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 📊 Metrics Collection
|
||||
|
||||
- **Prometheus Integration**: Native Prometheus metrics export
|
||||
- **Custom Metrics**: Application-specific performance metrics
|
||||
- **System Metrics**: CPU, memory, disk, and network monitoring
|
||||
- **Business Metrics**: Storage usage, request rates, and error tracking
|
||||
|
||||
### 🔍 Distributed Tracing
|
||||
|
||||
- **OpenTelemetry Support**: Standard distributed tracing
|
||||
- **Request Tracking**: End-to-end request lifecycle tracking
|
||||
- **Performance Analysis**: Latency and bottleneck identification
|
||||
- **Cross-Service Correlation**: Trace requests across microservices
|
||||
|
||||
### 📝 Structured Logging
|
||||
|
||||
- **JSON Logging**: Machine-readable structured logs
|
||||
- **Log Levels**: Configurable log levels and filtering
|
||||
- **Context Propagation**: Request context in all logs
|
||||
- **Log Aggregation**: Centralized log collection support
|
||||
|
||||
### 🚨 Alerting & Notifications
|
||||
|
||||
- **Rule-Based Alerts**: Configurable alerting rules
|
||||
- **Multiple Channels**: Email, Slack, webhook notifications
|
||||
- **Alert Escalation**: Tiered alerting and escalation policies
|
||||
- **Alert Correlation**: Group related alerts together
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-obs = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Observability Setup
|
||||
|
||||
```rust
|
||||
use rustfs_obs::{ObservabilityConfig, MetricsCollector, TracingProvider};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Configure observability
|
||||
let config = ObservabilityConfig {
|
||||
service_name: "rustfs-storage".to_string(),
|
||||
metrics_endpoint: "http://prometheus:9090".to_string(),
|
||||
tracing_endpoint: "http://jaeger:14268/api/traces".to_string(),
|
||||
log_level: "info".to_string(),
|
||||
enable_metrics: true,
|
||||
enable_tracing: true,
|
||||
};
|
||||
|
||||
// Initialize observability
|
||||
let obs = rustfs_obs::init(config).await?;
|
||||
|
||||
// Your application code here
|
||||
run_application().await?;
|
||||
|
||||
// Shutdown observability
|
||||
obs.shutdown().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Metrics Collection
|
||||
|
||||
```rust
|
||||
use rustfs_obs::metrics::{Counter, Histogram, Gauge, register_counter};
|
||||
|
||||
// Define metrics
|
||||
lazy_static! {
|
||||
static ref REQUESTS_TOTAL: Counter = register_counter!(
|
||||
"rustfs_requests_total",
|
||||
"Total number of requests",
|
||||
&["method", "status"]
|
||||
).unwrap();
|
||||
|
||||
static ref REQUEST_DURATION: Histogram = register_histogram!(
|
||||
"rustfs_request_duration_seconds",
|
||||
"Request duration in seconds",
|
||||
&["method"]
|
||||
).unwrap();
|
||||
|
||||
static ref ACTIVE_CONNECTIONS: Gauge = register_gauge!(
|
||||
"rustfs_active_connections",
|
||||
"Number of active connections"
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
async fn handle_request(method: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _timer = REQUEST_DURATION.with_label_values(&[method]).start_timer();
|
||||
|
||||
// Increment active connections
|
||||
ACTIVE_CONNECTIONS.inc();
|
||||
|
||||
// Simulate request processing
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// Record request completion
|
||||
REQUESTS_TOTAL.with_label_values(&[method, "success"]).inc();
|
||||
|
||||
// Decrement active connections
|
||||
ACTIVE_CONNECTIONS.dec();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Distributed Tracing
|
||||
|
||||
```rust
|
||||
use rustfs_obs::tracing::{trace_fn, Span, SpanContext};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
#[instrument(skip(data))]
|
||||
async fn process_upload(bucket: &str, key: &str, data: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let span = Span::current();
|
||||
span.set_attribute("bucket", bucket);
|
||||
span.set_attribute("key", key);
|
||||
span.set_attribute("size", data.len() as i64);
|
||||
|
||||
info!("Starting upload process");
|
||||
|
||||
// Validate data
|
||||
let validation_result = validate_data(data).await?;
|
||||
span.add_event("data_validated", &[("result", &validation_result)]);
|
||||
|
||||
// Store data
|
||||
let storage_result = store_data(bucket, key, data).await?;
|
||||
span.add_event("data_stored", &[("etag", &storage_result.etag)]);
|
||||
|
||||
// Update metadata
|
||||
update_metadata(bucket, key, &storage_result).await?;
|
||||
span.add_event("metadata_updated", &[]);
|
||||
|
||||
info!("Upload completed successfully");
|
||||
Ok(storage_result.etag)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn validate_data(data: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Validation logic
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
Ok("valid".to_string())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn store_data(bucket: &str, key: &str, data: &[u8]) -> Result<StorageResult, Box<dyn std::error::Error>> {
|
||||
// Storage logic
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
Ok(StorageResult {
|
||||
etag: "d41d8cd98f00b204e9800998ecf8427e".to_string(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```rust
|
||||
use rustfs_obs::logging::{LogEvent, LogLevel, StructuredLogger};
|
||||
use serde_json::json;
|
||||
|
||||
async fn logging_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let logger = StructuredLogger::new();
|
||||
|
||||
// Basic logging
|
||||
logger.info("Application started").await;
|
||||
|
||||
// Structured logging with context
|
||||
logger.log(LogEvent {
|
||||
level: LogLevel::Info,
|
||||
message: "Processing upload request".to_string(),
|
||||
context: json!({
|
||||
"bucket": "example-bucket",
|
||||
"key": "example-object",
|
||||
"size": 1024,
|
||||
"user_id": "user123",
|
||||
"request_id": "req-456"
|
||||
}),
|
||||
timestamp: chrono::Utc::now(),
|
||||
}).await;
|
||||
|
||||
// Error logging with details
|
||||
logger.error_with_context(
|
||||
"Failed to process upload",
|
||||
json!({
|
||||
"error_code": "STORAGE_FULL",
|
||||
"bucket": "example-bucket",
|
||||
"available_space": 0,
|
||||
"required_space": 1024
|
||||
})
|
||||
).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Alerting Configuration
|
||||
|
||||
```rust
|
||||
use rustfs_obs::alerting::{AlertManager, AlertRule, NotificationChannel};
|
||||
|
||||
async fn setup_alerting() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let alert_manager = AlertManager::new().await?;
|
||||
|
||||
// Configure notification channels
|
||||
let slack_channel = NotificationChannel::Slack {
|
||||
webhook_url: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK".to_string(),
|
||||
channel: "#rustfs-alerts".to_string(),
|
||||
};
|
||||
|
||||
let email_channel = NotificationChannel::Email {
|
||||
smtp_server: "smtp.example.com".to_string(),
|
||||
recipients: vec!["admin@example.com".to_string()],
|
||||
};
|
||||
|
||||
alert_manager.add_notification_channel("slack", slack_channel).await?;
|
||||
alert_manager.add_notification_channel("email", email_channel).await?;
|
||||
|
||||
// Define alert rules
|
||||
let high_error_rate = AlertRule {
|
||||
name: "high_error_rate".to_string(),
|
||||
description: "High error rate detected".to_string(),
|
||||
condition: "rate(rustfs_requests_total{status!=\"success\"}[5m]) > 0.1".to_string(),
|
||||
severity: "critical".to_string(),
|
||||
notifications: vec!["slack".to_string(), "email".to_string()],
|
||||
cooldown: Duration::from_minutes(15),
|
||||
};
|
||||
|
||||
let low_disk_space = AlertRule {
|
||||
name: "low_disk_space".to_string(),
|
||||
description: "Disk space running low".to_string(),
|
||||
condition: "rustfs_disk_usage_percent > 85".to_string(),
|
||||
severity: "warning".to_string(),
|
||||
notifications: vec!["slack".to_string()],
|
||||
cooldown: Duration::from_minutes(30),
|
||||
};
|
||||
|
||||
alert_manager.add_rule(high_error_rate).await?;
|
||||
alert_manager.add_rule(low_disk_space).await?;
|
||||
|
||||
// Start alert monitoring
|
||||
alert_manager.start().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
```rust
|
||||
use rustfs_obs::monitoring::{PerformanceMonitor, SystemMetrics, ApplicationMetrics};
|
||||
|
||||
async fn performance_monitoring() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let monitor = PerformanceMonitor::new().await?;
|
||||
|
||||
// Start system monitoring
|
||||
monitor.start_system_monitoring(Duration::from_secs(10)).await?;
|
||||
|
||||
// Custom application metrics
|
||||
let app_metrics = ApplicationMetrics::new();
|
||||
|
||||
// Monitor specific operations
|
||||
let upload_metrics = app_metrics.create_operation_monitor("upload");
|
||||
let download_metrics = app_metrics.create_operation_monitor("download");
|
||||
|
||||
// Simulate operations with monitoring
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
// Monitor upload operation
|
||||
let upload_timer = upload_metrics.start_timer();
|
||||
simulate_upload().await;
|
||||
upload_timer.record_success();
|
||||
|
||||
// Monitor download operation
|
||||
let download_timer = download_metrics.start_timer();
|
||||
match simulate_download().await {
|
||||
Ok(_) => download_timer.record_success(),
|
||||
Err(_) => download_timer.record_error(),
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Periodic metrics reporting
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let system_metrics = monitor.get_system_metrics().await;
|
||||
let app_metrics = monitor.get_application_metrics().await;
|
||||
|
||||
println!("=== System Metrics ===");
|
||||
println!("CPU Usage: {:.2}%", system_metrics.cpu_usage);
|
||||
println!("Memory Usage: {:.2}%", system_metrics.memory_usage);
|
||||
println!("Disk Usage: {:.2}%", system_metrics.disk_usage);
|
||||
|
||||
println!("=== Application Metrics ===");
|
||||
println!("Upload Throughput: {:.2} ops/sec", app_metrics.upload_throughput);
|
||||
println!("Download Throughput: {:.2} ops/sec", app_metrics.download_throughput);
|
||||
println!("Error Rate: {:.2}%", app_metrics.error_rate);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```rust
|
||||
use rustfs_obs::health::{HealthChecker, HealthStatus, HealthCheck};
|
||||
|
||||
async fn setup_health_checks() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let health_checker = HealthChecker::new();
|
||||
|
||||
// Add component health checks
|
||||
health_checker.add_check("database", Box::new(DatabaseHealthCheck)).await;
|
||||
health_checker.add_check("storage", Box::new(StorageHealthCheck)).await;
|
||||
health_checker.add_check("cache", Box::new(CacheHealthCheck)).await;
|
||||
|
||||
// Start health monitoring
|
||||
health_checker.start_monitoring(Duration::from_secs(30)).await?;
|
||||
|
||||
// Expose health endpoint
|
||||
health_checker.expose_http_endpoint("0.0.0.0:8080").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct DatabaseHealthCheck;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl HealthCheck for DatabaseHealthCheck {
|
||||
async fn check(&self) -> HealthStatus {
|
||||
// Perform database health check
|
||||
match check_database_connection().await {
|
||||
Ok(_) => HealthStatus::Healthy,
|
||||
Err(e) => HealthStatus::Unhealthy(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Observability Architecture
|
||||
|
||||
```
|
||||
Observability Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Observability API │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Metrics │ Tracing │ Logging │ Alerting │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Data Collection & Processing │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Prometheus │ OpenTelemetry │ Structured │ Alert Mgr │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ External Integrations │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Monitoring Stack
|
||||
|
||||
| Component | Purpose | Integration |
|
||||
|-----------|---------|-------------|
|
||||
| Prometheus | Metrics storage | Pull-based metrics collection |
|
||||
| Jaeger | Distributed tracing | OpenTelemetry traces |
|
||||
| Grafana | Visualization | Dashboards and alerts |
|
||||
| ELK Stack | Log aggregation | Structured log processing |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test metrics collection
|
||||
cargo test metrics
|
||||
|
||||
# Test tracing functionality
|
||||
cargo test tracing
|
||||
|
||||
# Test alerting
|
||||
cargo test alerting
|
||||
|
||||
# Integration tests
|
||||
cargo test --test integration
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **External Services**: Prometheus, Jaeger (optional)
|
||||
- **Network**: HTTP endpoint exposure capability
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Common](../common) - Common types and utilities
|
||||
- [RustFS Config](../config) - Configuration management
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Obs API Reference](https://docs.rustfs.com/obs/)
|
||||
- [Monitoring Guide](https://docs.rustfs.com/monitoring/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 📊 by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,590 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Policy Engine
|
||||
|
||||
<p align="center">
|
||||
<strong>Advanced policy-based access control engine for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Policy Engine** is a sophisticated access control system for the [RustFS](https://rustfs.com) distributed object storage platform. It provides fine-grained, attribute-based access control (ABAC) with support for complex policy expressions, dynamic evaluation, and AWS IAM-compatible policy syntax.
|
||||
|
||||
> **Note:** This is a core submodule of RustFS that provides essential access control and authorization capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔐 Access Control
|
||||
|
||||
- **AWS IAM Compatible**: Full support for AWS IAM policy syntax
|
||||
- **Fine-Grained Permissions**: Resource-level and action-level access control
|
||||
- **Dynamic Policy Evaluation**: Real-time policy evaluation with context
|
||||
- **Conditional Access**: Support for complex conditional expressions
|
||||
|
||||
### 📜 Policy Management
|
||||
|
||||
- **Policy Documents**: Structured policy definition and management
|
||||
- **Policy Versioning**: Version control for policy documents
|
||||
- **Policy Validation**: Syntax and semantic validation
|
||||
- **Policy Templates**: Pre-built policy templates for common use cases
|
||||
|
||||
### 🎯 Advanced Features
|
||||
|
||||
- **Attribute-Based Access Control (ABAC)**: Context-aware access decisions
|
||||
- **Function-Based Conditions**: Rich set of condition functions
|
||||
- **Principal-Based Policies**: User, group, and service account policies
|
||||
- **Resource-Based Policies**: Bucket and object-level policies
|
||||
|
||||
### 🛠️ Integration Features
|
||||
|
||||
- **ARN Support**: AWS-style Amazon Resource Names
|
||||
- **Multi-Tenant Support**: Isolated policy evaluation per tenant
|
||||
- **Real-Time Evaluation**: High-performance policy evaluation engine
|
||||
- **Audit Trail**: Comprehensive policy evaluation logging
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Policy Engine Architecture
|
||||
|
||||
```
|
||||
Policy Engine Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Policy API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Policy Parser │ Policy Validator │ Policy Store │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Policy Evaluation Engine │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Condition Functions │ Principal Resolver │ Resource Mgr │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Authentication Integration │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Policy Decision Flow
|
||||
|
||||
```
|
||||
Policy Decision Flow:
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Request │───▶│ Policy │───▶│ Decision │
|
||||
│ (Subject, │ │ Evaluation │ │ (Allow/ │
|
||||
│ Action, │ │ Engine │ │ Deny/ │
|
||||
│ Resource) │ │ │ │ Not Found) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Context │ │ Condition │ │ Audit │
|
||||
│ Information │ │ Functions │ │ Log │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-policy = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Policy Creation
|
||||
|
||||
```rust
|
||||
use rustfs_policy::policy::{Policy, Statement, Effect, Action, Resource};
|
||||
use rustfs_policy::arn::ARN;
|
||||
use serde_json::json;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a simple policy
|
||||
let policy = Policy::new(
|
||||
"2012-10-17".to_string(),
|
||||
vec![
|
||||
Statement::new(
|
||||
Effect::Allow,
|
||||
vec![Action::from_str("s3:GetObject")?],
|
||||
vec![Resource::from_str("arn:aws:s3:::my-bucket/*")?],
|
||||
None, // No conditions
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Serialize to JSON
|
||||
let policy_json = serde_json::to_string_pretty(&policy)?;
|
||||
println!("Policy JSON:\n{}", policy_json);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Policy with Conditions
|
||||
|
||||
```rust
|
||||
use rustfs_policy::policy::{Policy, Statement, Effect, Action, Resource};
|
||||
use rustfs_policy::policy::function::{Function, FunctionName};
|
||||
use serde_json::json;
|
||||
|
||||
fn create_conditional_policy() -> Result<Policy, Box<dyn std::error::Error>> {
|
||||
// Create a policy with IP address restrictions
|
||||
let policy = Policy::new(
|
||||
"2012-10-17".to_string(),
|
||||
vec![
|
||||
Statement::builder()
|
||||
.effect(Effect::Allow)
|
||||
.action(Action::from_str("s3:GetObject")?)
|
||||
.resource(Resource::from_str("arn:aws:s3:::secure-bucket/*")?)
|
||||
.condition(
|
||||
"IpAddress",
|
||||
"aws:SourceIp",
|
||||
json!(["192.168.1.0/24", "10.0.0.0/8"])
|
||||
)
|
||||
.build(),
|
||||
Statement::builder()
|
||||
.effect(Effect::Deny)
|
||||
.action(Action::from_str("s3:*")?)
|
||||
.resource(Resource::from_str("arn:aws:s3:::secure-bucket/*")?)
|
||||
.condition(
|
||||
"DateGreaterThan",
|
||||
"aws:CurrentTime",
|
||||
json!("2024-12-31T23:59:59Z")
|
||||
)
|
||||
.build(),
|
||||
],
|
||||
);
|
||||
|
||||
Ok(policy)
|
||||
}
|
||||
```
|
||||
|
||||
### Policy Evaluation
|
||||
|
||||
```rust
|
||||
use rustfs_policy::policy::{Policy, PolicyDoc};
|
||||
use rustfs_policy::auth::{Identity, Request};
|
||||
use rustfs_policy::arn::ARN;
|
||||
|
||||
async fn evaluate_policy_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Load policy from storage
|
||||
let policy_doc = PolicyDoc::try_from(policy_bytes)?;
|
||||
|
||||
// Create evaluation context
|
||||
let identity = Identity::new(
|
||||
"user123".to_string(),
|
||||
vec!["group1".to_string(), "group2".to_string()],
|
||||
);
|
||||
|
||||
let request = Request::new(
|
||||
"s3:GetObject".to_string(),
|
||||
ARN::from_str("arn:aws:s3:::my-bucket/file.txt")?,
|
||||
Some(json!({
|
||||
"aws:SourceIp": "192.168.1.100",
|
||||
"aws:CurrentTime": "2024-01-15T10:30:00Z"
|
||||
})),
|
||||
);
|
||||
|
||||
// Evaluate policy
|
||||
let result = policy_doc.policy.evaluate(&identity, &request).await?;
|
||||
|
||||
match result {
|
||||
Effect::Allow => println!("Access allowed"),
|
||||
Effect::Deny => println!("Access denied"),
|
||||
Effect::NotEvaluated => println!("No applicable policy found"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Policy Templates
|
||||
|
||||
```rust
|
||||
use rustfs_policy::policy::{Policy, Statement, Effect};
|
||||
use rustfs_policy::templates::PolicyTemplate;
|
||||
|
||||
fn create_common_policies() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Read-only policy template
|
||||
let read_only_policy = PolicyTemplate::read_only_bucket("my-bucket")?;
|
||||
|
||||
// Full access policy template
|
||||
let full_access_policy = PolicyTemplate::full_access_bucket("my-bucket")?;
|
||||
|
||||
// Admin policy template
|
||||
let admin_policy = PolicyTemplate::admin_all_resources()?;
|
||||
|
||||
// Custom policy with multiple permissions
|
||||
let custom_policy = Policy::builder()
|
||||
.version("2012-10-17")
|
||||
.statement(
|
||||
Statement::builder()
|
||||
.effect(Effect::Allow)
|
||||
.action("s3:GetObject")
|
||||
.action("s3:PutObject")
|
||||
.resource("arn:aws:s3:::uploads/*")
|
||||
.condition("StringEquals", "s3:x-amz-acl", "bucket-owner-full-control")
|
||||
.build()
|
||||
)
|
||||
.statement(
|
||||
Statement::builder()
|
||||
.effect(Effect::Allow)
|
||||
.action("s3:ListBucket")
|
||||
.resource("arn:aws:s3:::uploads")
|
||||
.condition("StringLike", "s3:prefix", "user/${aws:username}/*")
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Resource-Based Policies
|
||||
|
||||
```rust
|
||||
use rustfs_policy::policy::{Policy, Statement, Effect, Principal};
|
||||
use rustfs_policy::arn::ARN;
|
||||
|
||||
fn create_resource_policy() -> Result<Policy, Box<dyn std::error::Error>> {
|
||||
// Create a bucket policy allowing cross-account access
|
||||
let bucket_policy = Policy::builder()
|
||||
.version("2012-10-17")
|
||||
.statement(
|
||||
Statement::builder()
|
||||
.effect(Effect::Allow)
|
||||
.principal(Principal::AWS("arn:aws:iam::123456789012:root".to_string()))
|
||||
.action("s3:GetObject")
|
||||
.resource("arn:aws:s3:::shared-bucket/*")
|
||||
.condition("StringEquals", "s3:ExistingObjectTag/Department", "Finance")
|
||||
.build()
|
||||
)
|
||||
.statement(
|
||||
Statement::builder()
|
||||
.effect(Effect::Allow)
|
||||
.principal(Principal::AWS("arn:aws:iam::123456789012:user/john".to_string()))
|
||||
.action("s3:PutObject")
|
||||
.resource("arn:aws:s3:::shared-bucket/uploads/*")
|
||||
.condition("StringEquals", "s3:x-amz-acl", "bucket-owner-full-control")
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
Ok(bucket_policy)
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run policy evaluation tests
|
||||
cargo test policy_evaluation
|
||||
|
||||
# Run condition function tests
|
||||
cargo test condition_functions
|
||||
|
||||
# Run ARN parsing tests
|
||||
cargo test arn_parsing
|
||||
|
||||
# Run policy validation tests
|
||||
cargo test policy_validation
|
||||
|
||||
# Run with test coverage
|
||||
cargo test --features test-coverage
|
||||
```
|
||||
|
||||
## 📋 Policy Syntax
|
||||
|
||||
### Basic Policy Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::my-bucket/*"
|
||||
],
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:x-amz-acl": "bucket-owner-full-control"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Actions
|
||||
|
||||
| Action Category | Actions | Description |
|
||||
|----------------|---------|-------------|
|
||||
| Object Operations | `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject` | Object-level operations |
|
||||
| Bucket Operations | `s3:ListBucket`, `s3:CreateBucket`, `s3:DeleteBucket` | Bucket-level operations |
|
||||
| Access Control | `s3:GetBucketAcl`, `s3:PutBucketAcl` | Access control operations |
|
||||
| Versioning | `s3:GetObjectVersion`, `s3:DeleteObjectVersion` | Object versioning operations |
|
||||
| Multipart Upload | `s3:ListMultipartUploads`, `s3:AbortMultipartUpload` | Multipart upload operations |
|
||||
|
||||
### Condition Functions
|
||||
|
||||
| Function | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `StringEquals` | Exact string matching | `"StringEquals": {"s3:x-amz-acl": "private"}` |
|
||||
| `StringLike` | Wildcard string matching | `"StringLike": {"s3:prefix": "photos/*"}` |
|
||||
| `IpAddress` | IP address/CIDR matching | `"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}` |
|
||||
| `DateGreaterThan` | Date comparison | `"DateGreaterThan": {"aws:CurrentTime": "2024-01-01T00:00:00Z"}` |
|
||||
| `NumericEquals` | Numeric comparison | `"NumericEquals": {"s3:max-keys": "100"}` |
|
||||
| `Bool` | Boolean comparison | `"Bool": {"aws:SecureTransport": "true"}` |
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Policy Engine Configuration
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
# Policy evaluation settings
|
||||
max_policy_size = 2048 # Maximum policy size in KB
|
||||
max_conditions_per_statement = 10
|
||||
max_statements_per_policy = 100
|
||||
|
||||
# Performance settings
|
||||
cache_policy_documents = true
|
||||
cache_ttl_seconds = 300
|
||||
max_cached_policies = 1000
|
||||
|
||||
# Security settings
|
||||
require_secure_transport = true
|
||||
allow_anonymous_access = false
|
||||
default_effect = "deny"
|
||||
|
||||
# Audit settings
|
||||
audit_policy_evaluation = true
|
||||
audit_log_path = "/var/log/rustfs/policy-audit.log"
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```rust
|
||||
use rustfs_policy::config::PolicyConfig;
|
||||
|
||||
let config = PolicyConfig {
|
||||
// Policy parsing settings
|
||||
max_policy_size_kb: 2048,
|
||||
max_conditions_per_statement: 10,
|
||||
max_statements_per_policy: 100,
|
||||
|
||||
// Evaluation settings
|
||||
default_effect: Effect::Deny,
|
||||
require_explicit_deny: false,
|
||||
cache_policy_documents: true,
|
||||
cache_ttl_seconds: 300,
|
||||
|
||||
// Security settings
|
||||
require_secure_transport: true,
|
||||
allow_anonymous_access: false,
|
||||
validate_resource_arns: true,
|
||||
|
||||
// Performance settings
|
||||
max_cached_policies: 1000,
|
||||
evaluation_timeout_ms: 100,
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
```
|
||||
|
||||
## 🚀 Performance Optimization
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
- **Policy Document Cache**: Cache parsed policy documents
|
||||
- **Evaluation Result Cache**: Cache evaluation results for identical requests
|
||||
- **Condition Cache**: Cache condition function results
|
||||
- **Principal Cache**: Cache principal resolution results
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Minimize Policy Size**: Keep policies as small as possible
|
||||
2. **Use Specific Actions**: Avoid overly broad action patterns
|
||||
3. **Optimize Conditions**: Use efficient condition functions
|
||||
4. **Cache Frequently Used Policies**: Enable policy caching for better performance
|
||||
|
||||
## 🤝 Integration with RustFS
|
||||
|
||||
The Policy Engine integrates seamlessly with other RustFS components:
|
||||
|
||||
- **IAM Module**: Provides policy storage and user/group management
|
||||
- **ECStore**: Implements resource-based access control
|
||||
- **API Server**: Enforces policies on S3 API operations
|
||||
- **Audit System**: Logs policy evaluation decisions
|
||||
- **Admin Interface**: Manages policy documents and templates
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Memory**: Minimum 1GB RAM for policy caching
|
||||
- **Storage**: Compatible with RustFS storage backend
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Policy Parse Errors**:
|
||||
- Check JSON syntax validity
|
||||
- Verify action and resource ARN formats
|
||||
- Validate condition function syntax
|
||||
|
||||
2. **Policy Evaluation Failures**:
|
||||
- Check principal resolution
|
||||
- Verify resource ARN matching
|
||||
- Debug condition function evaluation
|
||||
|
||||
3. **Performance Issues**:
|
||||
- Monitor policy cache hit rates
|
||||
- Check policy document sizes
|
||||
- Optimize condition functions
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Validate policy syntax
|
||||
rustfs-cli policy validate --file policy.json
|
||||
|
||||
# Test policy evaluation
|
||||
rustfs-cli policy test --policy policy.json --user john --action s3:GetObject --resource arn:aws:s3:::bucket/key
|
||||
|
||||
# Show policy evaluation trace
|
||||
rustfs-cli policy trace --policy policy.json --user john --action s3:GetObject --resource arn:aws:s3:::bucket/key
|
||||
```
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS IAM](../iam) - Identity and access management
|
||||
- [RustFS ECStore](../ecstore) - Erasure coding storage engine
|
||||
- [RustFS Crypto](../crypto) - Cryptographic operations
|
||||
- [RustFS Utils](../utils) - Utility functions
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Policy Engine API Reference](https://docs.rustfs.com/policy/)
|
||||
- [Policy Language Guide](https://docs.rustfs.com/policy-language/)
|
||||
- [Access Control Guide](https://docs.rustfs.com/access-control/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details on:
|
||||
|
||||
- Policy engine architecture and design patterns
|
||||
- Policy language syntax and semantics
|
||||
- Condition function implementation
|
||||
- Performance optimization techniques
|
||||
- Security considerations for access control
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/rustfs/rustfs.git
|
||||
cd rustfs
|
||||
|
||||
# Navigate to Policy module
|
||||
cd crates/policy
|
||||
|
||||
# Install dependencies
|
||||
cargo build
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Run policy validation tests
|
||||
cargo test policy_validation
|
||||
|
||||
# Format code
|
||||
cargo fmt
|
||||
|
||||
# Run linter
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
## 💬 Getting Help
|
||||
|
||||
- **Documentation**: [docs.rustfs.com](https://docs.rustfs.com)
|
||||
- **Issues**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
- **Security**: Report security issues to <security@rustfs.com>
|
||||
|
||||
## 📞 Contact
|
||||
|
||||
- **Bugs**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||
- **Business**: <hello@rustfs.com>
|
||||
- **Jobs**: <jobs@rustfs.com>
|
||||
- **General Discussion**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
This module is maintained by the RustFS security team and community contributors. Special thanks to all who have contributed to making RustFS access control robust and flexible.
|
||||
|
||||
<a href="https://github.com/rustfs/rustfs/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=rustfs/rustfs" />
|
||||
</a>
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🛡️ by the RustFS Security Team
|
||||
</p>
|
||||
@@ -0,0 +1,426 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Protos - Protocol Buffer Definitions
|
||||
|
||||
<p align="center">
|
||||
<strong>Protocol buffer definitions and gRPC interfaces for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Protos** provides protocol buffer definitions and gRPC service interfaces for the [RustFS](https://rustfs.com) distributed object storage system. It defines the communication protocols, message formats, and service contracts used across all RustFS components.
|
||||
|
||||
> **Note:** This is a foundational submodule of RustFS that provides essential communication protocols for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 📡 gRPC Services
|
||||
|
||||
- **Storage Service**: Core storage operations (get, put, delete)
|
||||
- **Admin Service**: Administrative and management operations
|
||||
- **Metadata Service**: Metadata management and queries
|
||||
- **Lock Service**: Distributed locking and coordination
|
||||
|
||||
### 📦 Message Types
|
||||
|
||||
- **Storage Messages**: Object and bucket operation messages
|
||||
- **Administrative Messages**: Cluster management messages
|
||||
- **Metadata Messages**: File and object metadata structures
|
||||
- **Error Messages**: Standardized error reporting
|
||||
|
||||
### 🔧 Protocol Features
|
||||
|
||||
- **Versioning**: Protocol version compatibility management
|
||||
- **Extensions**: Custom field extensions for future expansion
|
||||
- **Streaming**: Support for streaming large data transfers
|
||||
- **Compression**: Built-in message compression support
|
||||
|
||||
### 🛠️ Code Generation
|
||||
|
||||
- **Rust Bindings**: Automatic Rust code generation
|
||||
- **Type Safety**: Strong typing for all protocol messages
|
||||
- **Documentation**: Generated API documentation
|
||||
- **Validation**: Message validation and constraints
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-protos = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic gRPC Client
|
||||
|
||||
```rust
|
||||
use rustfs_protos::storage::{StorageServiceClient, GetObjectRequest, PutObjectRequest};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Connect to storage service
|
||||
let channel = Channel::from_static("http://storage.rustfs.local:9000")
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
let mut client = StorageServiceClient::new(channel);
|
||||
|
||||
// Get object
|
||||
let request = GetObjectRequest {
|
||||
bucket: "example-bucket".to_string(),
|
||||
key: "example-object".to_string(),
|
||||
version_id: None,
|
||||
range: None,
|
||||
};
|
||||
|
||||
let response = client.get_object(request).await?;
|
||||
let object_data = response.into_inner();
|
||||
|
||||
println!("Retrieved object: {} bytes", object_data.content.len());
|
||||
println!("Content type: {}", object_data.content_type);
|
||||
println!("ETag: {}", object_data.etag);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Storage Operations
|
||||
|
||||
```rust
|
||||
use rustfs_protos::storage::{
|
||||
StorageServiceClient, PutObjectRequest, DeleteObjectRequest,
|
||||
ListObjectsRequest, CreateBucketRequest
|
||||
};
|
||||
|
||||
async fn storage_operations_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut client = StorageServiceClient::connect("http://storage.rustfs.local:9000").await?;
|
||||
|
||||
// Create bucket
|
||||
let create_bucket_request = CreateBucketRequest {
|
||||
bucket: "new-bucket".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
acl: None,
|
||||
storage_class: "STANDARD".to_string(),
|
||||
};
|
||||
|
||||
client.create_bucket(create_bucket_request).await?;
|
||||
println!("Bucket created successfully");
|
||||
|
||||
// Put object
|
||||
let put_request = PutObjectRequest {
|
||||
bucket: "new-bucket".to_string(),
|
||||
key: "test-file.txt".to_string(),
|
||||
content: b"Hello, RustFS!".to_vec(),
|
||||
content_type: "text/plain".to_string(),
|
||||
metadata: std::collections::HashMap::new(),
|
||||
storage_class: None,
|
||||
};
|
||||
|
||||
let put_response = client.put_object(put_request).await?;
|
||||
println!("Object uploaded, ETag: {}", put_response.into_inner().etag);
|
||||
|
||||
// List objects
|
||||
let list_request = ListObjectsRequest {
|
||||
bucket: "new-bucket".to_string(),
|
||||
prefix: None,
|
||||
marker: None,
|
||||
max_keys: Some(100),
|
||||
delimiter: None,
|
||||
};
|
||||
|
||||
let list_response = client.list_objects(list_request).await?;
|
||||
let objects = list_response.into_inner();
|
||||
|
||||
for object in objects.contents {
|
||||
println!("Object: {} (size: {} bytes)", object.key, object.size);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Administrative Operations
|
||||
|
||||
```rust
|
||||
use rustfs_protos::admin::{
|
||||
AdminServiceClient, GetServerInfoRequest, AddServerRequest,
|
||||
ListServersRequest, ConfigUpdateRequest
|
||||
};
|
||||
|
||||
async fn admin_operations_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut client = AdminServiceClient::connect("http://admin.rustfs.local:9001").await?;
|
||||
|
||||
// Get server information
|
||||
let info_request = GetServerInfoRequest {};
|
||||
let info_response = client.get_server_info(info_request).await?;
|
||||
let server_info = info_response.into_inner();
|
||||
|
||||
println!("Server version: {}", server_info.version);
|
||||
println!("Uptime: {} seconds", server_info.uptime_seconds);
|
||||
println!("Memory usage: {} MB", server_info.memory_usage_mb);
|
||||
|
||||
// List cluster servers
|
||||
let list_request = ListServersRequest {};
|
||||
let list_response = client.list_servers(list_request).await?;
|
||||
let servers = list_response.into_inner();
|
||||
|
||||
for server in servers.servers {
|
||||
println!("Server: {} - Status: {}", server.endpoint, server.status);
|
||||
}
|
||||
|
||||
// Add new server
|
||||
let add_request = AddServerRequest {
|
||||
endpoint: "https://new-node.rustfs.local:9000".to_string(),
|
||||
access_key: "node-access-key".to_string(),
|
||||
secret_key: "node-secret-key".to_string(),
|
||||
};
|
||||
|
||||
client.add_server(add_request).await?;
|
||||
println!("New server added to cluster");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Metadata Operations
|
||||
|
||||
```rust
|
||||
use rustfs_protos::metadata::{
|
||||
MetadataServiceClient, SearchObjectsRequest, GetObjectMetadataRequest,
|
||||
UpdateObjectMetadataRequest
|
||||
};
|
||||
|
||||
async fn metadata_operations_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut client = MetadataServiceClient::connect("http://metadata.rustfs.local:9002").await?;
|
||||
|
||||
// Search objects
|
||||
let search_request = SearchObjectsRequest {
|
||||
query: "content_type:image/*".to_string(),
|
||||
bucket: Some("photos-bucket".to_string()),
|
||||
limit: Some(50),
|
||||
offset: Some(0),
|
||||
};
|
||||
|
||||
let search_response = client.search_objects(search_request).await?;
|
||||
let results = search_response.into_inner();
|
||||
|
||||
for object in results.objects {
|
||||
println!("Found: {} ({})", object.key, object.content_type);
|
||||
}
|
||||
|
||||
// Get object metadata
|
||||
let metadata_request = GetObjectMetadataRequest {
|
||||
bucket: "photos-bucket".to_string(),
|
||||
key: "vacation-photo.jpg".to_string(),
|
||||
};
|
||||
|
||||
let metadata_response = client.get_object_metadata(metadata_request).await?;
|
||||
let metadata = metadata_response.into_inner();
|
||||
|
||||
println!("Object metadata:");
|
||||
for (key, value) in metadata.metadata {
|
||||
println!(" {}: {}", key, value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Lock Service Operations
|
||||
|
||||
```rust
|
||||
use rustfs_protos::lock::{
|
||||
LockServiceClient, AcquireLockRequest, ReleaseLockRequest,
|
||||
LockType, LockMode
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
async fn lock_operations_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut client = LockServiceClient::connect("http://lock.rustfs.local:9003").await?;
|
||||
|
||||
// Acquire distributed lock
|
||||
let acquire_request = AcquireLockRequest {
|
||||
resource_id: "bucket/important-data".to_string(),
|
||||
lock_type: LockType::Exclusive as i32,
|
||||
timeout_seconds: 30,
|
||||
auto_renew: true,
|
||||
};
|
||||
|
||||
let acquire_response = client.acquire_lock(acquire_request).await?;
|
||||
let lock_token = acquire_response.into_inner().lock_token;
|
||||
|
||||
println!("Lock acquired: {}", lock_token);
|
||||
|
||||
// Perform critical operations
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
|
||||
// Release lock
|
||||
let release_request = ReleaseLockRequest {
|
||||
lock_token: lock_token.clone(),
|
||||
};
|
||||
|
||||
client.release_lock(release_request).await?;
|
||||
println!("Lock released: {}", lock_token);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Operations
|
||||
|
||||
```rust
|
||||
use rustfs_protos::storage::{StorageServiceClient, StreamUploadRequest, StreamDownloadRequest};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
async fn streaming_operations_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut client = StorageServiceClient::connect("http://storage.rustfs.local:9000").await?;
|
||||
|
||||
// Streaming upload
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(100);
|
||||
let request_stream = ReceiverStream::new(rx);
|
||||
|
||||
// Send upload metadata
|
||||
tx.send(StreamUploadRequest {
|
||||
bucket: "large-files".to_string(),
|
||||
key: "big-video.mp4".to_string(),
|
||||
content_type: "video/mp4".to_string(),
|
||||
chunk: vec![], // Empty chunk for metadata
|
||||
}).await?;
|
||||
|
||||
// Send file chunks
|
||||
let mut file = tokio::fs::File::open("big-video.mp4").await?;
|
||||
let mut buffer = vec![0u8; 64 * 1024]; // 64KB chunks
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match file.read(&mut buffer).await {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(n) => {
|
||||
let chunk_request = StreamUploadRequest {
|
||||
bucket: String::new(),
|
||||
key: String::new(),
|
||||
content_type: String::new(),
|
||||
chunk: buffer[..n].to_vec(),
|
||||
};
|
||||
|
||||
if tx.send(chunk_request).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let upload_response = client.stream_upload(request_stream).await?;
|
||||
println!("Upload completed: {}", upload_response.into_inner().etag);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Protocol Architecture
|
||||
|
||||
```
|
||||
Protocol Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ gRPC Services │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Storage API │ Admin API │ Metadata API │ Lock API │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Protocol Buffer Messages │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Requests │ Responses │ Streaming │ Errors │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Transport Layer (HTTP/2) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Service Definitions
|
||||
|
||||
| Service | Purpose | Key Operations |
|
||||
|---------|---------|----------------|
|
||||
| Storage | Object operations | Get, Put, Delete, List |
|
||||
| Admin | Cluster management | Add/Remove nodes, Config |
|
||||
| Metadata | Metadata queries | Search, Index, Update |
|
||||
| Lock | Distributed locking | Acquire, Release, Renew |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test protocol buffer compilation
|
||||
cargo test proto_compilation
|
||||
|
||||
# Test service interfaces
|
||||
cargo test service_interfaces
|
||||
|
||||
# Test message serialization
|
||||
cargo test serialization
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Dependencies**: Protocol Buffers compiler (protoc)
|
||||
- **Network**: gRPC transport support
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Common](../common) - Common types and utilities
|
||||
- [RustFS Lock](../lock) - Distributed locking
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Protos API Reference](https://docs.rustfs.com/protos/)
|
||||
- [gRPC Guide](https://docs.rustfs.com/grpc/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 📡 by the RustFS Team
|
||||
</p>
|
||||
@@ -40,5 +40,5 @@ serde_json.workspace = true
|
||||
md-5 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5.1", features = ["async", "async_tokio", "tokio"] }
|
||||
#criterion = { version = "0.5.1", features = ["async", "async_tokio", "tokio"] }
|
||||
tokio-test = "0.4"
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Rio - High-Performance I/O
|
||||
|
||||
<p align="center">
|
||||
<strong>High-performance asynchronous I/O operations for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Rio** provides high-performance asynchronous I/O operations for the [RustFS](https://rustfs.com) distributed object storage system. It implements efficient data streaming, encryption, compression, and integrity checking with zero-copy operations and optimized buffering strategies.
|
||||
|
||||
> **Note:** This is a performance-critical submodule of RustFS that provides essential I/O capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🚀 High-Performance I/O
|
||||
|
||||
- **Zero-Copy Operations**: Efficient data movement without unnecessary copying
|
||||
- **Async Streaming**: Non-blocking streaming I/O with backpressure handling
|
||||
- **Vectored I/O**: Scatter-gather operations for improved throughput
|
||||
- **Buffer Management**: Intelligent buffer pooling and reuse
|
||||
|
||||
### 🔐 Cryptographic Operations
|
||||
|
||||
- **AES-GCM Encryption**: Hardware-accelerated encryption/decryption
|
||||
- **Streaming Encryption**: Encrypt data on-the-fly without buffering
|
||||
- **Key Management**: Secure key derivation and rotation
|
||||
- **Digital Signatures**: Data integrity verification
|
||||
|
||||
### 📦 Compression Support
|
||||
|
||||
- **Multi-Algorithm**: Support for various compression algorithms
|
||||
- **Streaming Compression**: Real-time compression during transfer
|
||||
- **Adaptive Compression**: Dynamic algorithm selection based on data
|
||||
- **Compression Levels**: Configurable compression vs. speed tradeoffs
|
||||
|
||||
### 🔧 Data Integrity
|
||||
|
||||
- **CRC32 Checksums**: Fast integrity checking
|
||||
- **MD5 Hashing**: Legacy compatibility and verification
|
||||
- **Merkle Trees**: Hierarchical integrity verification
|
||||
- **Error Correction**: Automatic error detection and correction
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-rio = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Streaming I/O
|
||||
|
||||
```rust
|
||||
use rustfs_rio::{StreamReader, StreamWriter, BufferPool};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create buffer pool for efficient memory management
|
||||
let buffer_pool = BufferPool::new(64 * 1024, 100); // 64KB buffers, 100 in pool
|
||||
|
||||
// Create streaming reader
|
||||
let mut reader = StreamReader::new(input_source, buffer_pool.clone());
|
||||
|
||||
// Create streaming writer
|
||||
let mut writer = StreamWriter::new(output_destination, buffer_pool.clone());
|
||||
|
||||
// High-performance streaming copy
|
||||
let mut buffer = vec![0u8; 8192];
|
||||
loop {
|
||||
let n = reader.read(&mut buffer).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_all(&buffer[..n]).await?;
|
||||
}
|
||||
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Encrypted Streaming
|
||||
|
||||
```rust
|
||||
use rustfs_rio::{EncryptedWriter, EncryptedReader, EncryptionKey};
|
||||
use aes_gcm::{Aes256Gcm, Key, Nonce};
|
||||
|
||||
async fn encrypted_streaming_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Generate encryption key
|
||||
let key = EncryptionKey::generate()?;
|
||||
|
||||
// Create encrypted writer
|
||||
let mut encrypted_writer = EncryptedWriter::new(
|
||||
output_stream,
|
||||
key.clone(),
|
||||
Aes256Gcm::new(&key.into())
|
||||
)?;
|
||||
|
||||
// Write encrypted data
|
||||
encrypted_writer.write_all(b"Hello, encrypted world!").await?;
|
||||
encrypted_writer.finalize().await?;
|
||||
|
||||
// Create encrypted reader
|
||||
let mut encrypted_reader = EncryptedReader::new(
|
||||
input_stream,
|
||||
key.clone(),
|
||||
Aes256Gcm::new(&key.into())
|
||||
)?;
|
||||
|
||||
// Read decrypted data
|
||||
let mut decrypted_data = Vec::new();
|
||||
encrypted_reader.read_to_end(&mut decrypted_data).await?;
|
||||
|
||||
println!("Decrypted: {}", String::from_utf8(decrypted_data)?);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Compressed Streaming
|
||||
|
||||
```rust
|
||||
use rustfs_rio::{CompressedWriter, CompressedReader, CompressionAlgorithm};
|
||||
|
||||
async fn compressed_streaming_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create compressed writer
|
||||
let mut compressed_writer = CompressedWriter::new(
|
||||
output_stream,
|
||||
CompressionAlgorithm::Zstd,
|
||||
6 // compression level
|
||||
)?;
|
||||
|
||||
// Write compressed data
|
||||
compressed_writer.write_all(b"This data will be compressed").await?;
|
||||
compressed_writer.write_all(b"and streamed efficiently").await?;
|
||||
compressed_writer.finish().await?;
|
||||
|
||||
// Create compressed reader
|
||||
let mut compressed_reader = CompressedReader::new(
|
||||
input_stream,
|
||||
CompressionAlgorithm::Zstd
|
||||
)?;
|
||||
|
||||
// Read decompressed data
|
||||
let mut decompressed_data = Vec::new();
|
||||
compressed_reader.read_to_end(&mut decompressed_data).await?;
|
||||
|
||||
println!("Decompressed: {}", String::from_utf8(decompressed_data)?);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Integrity Checking
|
||||
|
||||
```rust
|
||||
use rustfs_rio::{ChecksumWriter, ChecksumReader, ChecksumAlgorithm};
|
||||
|
||||
async fn integrity_checking_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create checksum writer
|
||||
let mut checksum_writer = ChecksumWriter::new(
|
||||
output_stream,
|
||||
ChecksumAlgorithm::Crc32
|
||||
);
|
||||
|
||||
// Write data with checksum calculation
|
||||
checksum_writer.write_all(b"Data with integrity checking").await?;
|
||||
let write_checksum = checksum_writer.finalize().await?;
|
||||
|
||||
println!("Write checksum: {:08x}", write_checksum);
|
||||
|
||||
// Create checksum reader
|
||||
let mut checksum_reader = ChecksumReader::new(
|
||||
input_stream,
|
||||
ChecksumAlgorithm::Crc32
|
||||
);
|
||||
|
||||
// Read data with checksum verification
|
||||
let mut data = Vec::new();
|
||||
checksum_reader.read_to_end(&mut data).await?;
|
||||
let read_checksum = checksum_reader.checksum();
|
||||
|
||||
println!("Read checksum: {:08x}", read_checksum);
|
||||
|
||||
// Verify integrity
|
||||
if write_checksum == read_checksum {
|
||||
println!("Data integrity verified!");
|
||||
} else {
|
||||
println!("Data corruption detected!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Layer Streaming
|
||||
|
||||
```rust
|
||||
use rustfs_rio::{MultiLayerWriter, MultiLayerReader, Layer};
|
||||
|
||||
async fn multi_layer_streaming_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create multi-layer writer (compression + encryption + checksum)
|
||||
let mut writer = MultiLayerWriter::new(output_stream)
|
||||
.add_layer(Layer::Compression(CompressionAlgorithm::Zstd, 6))
|
||||
.add_layer(Layer::Encryption(encryption_key.clone()))
|
||||
.add_layer(Layer::Checksum(ChecksumAlgorithm::Crc32))
|
||||
.build()?;
|
||||
|
||||
// Write data through all layers
|
||||
writer.write_all(b"This data will be compressed, encrypted, and checksummed").await?;
|
||||
let final_checksum = writer.finalize().await?;
|
||||
|
||||
// Create multi-layer reader (reverse order)
|
||||
let mut reader = MultiLayerReader::new(input_stream)
|
||||
.add_layer(Layer::Checksum(ChecksumAlgorithm::Crc32))
|
||||
.add_layer(Layer::Decryption(encryption_key.clone()))
|
||||
.add_layer(Layer::Decompression(CompressionAlgorithm::Zstd))
|
||||
.build()?;
|
||||
|
||||
// Read data through all layers
|
||||
let mut data = Vec::new();
|
||||
reader.read_to_end(&mut data).await?;
|
||||
|
||||
// Verify final checksum
|
||||
if reader.verify_checksum(final_checksum)? {
|
||||
println!("All layers verified successfully!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Vectored I/O Operations
|
||||
|
||||
```rust
|
||||
use rustfs_rio::{VectoredWriter, VectoredReader, IoVec};
|
||||
|
||||
async fn vectored_io_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create vectored writer
|
||||
let mut vectored_writer = VectoredWriter::new(output_stream);
|
||||
|
||||
// Prepare multiple buffers
|
||||
let header = b"HEADER";
|
||||
let data = b"Important data content";
|
||||
let footer = b"FOOTER";
|
||||
|
||||
// Write multiple buffers in one operation
|
||||
let io_vecs = vec![
|
||||
IoVec::new(header),
|
||||
IoVec::new(data),
|
||||
IoVec::new(footer),
|
||||
];
|
||||
|
||||
let bytes_written = vectored_writer.write_vectored(&io_vecs).await?;
|
||||
println!("Wrote {} bytes in vectored operation", bytes_written);
|
||||
|
||||
// Create vectored reader
|
||||
let mut vectored_reader = VectoredReader::new(input_stream);
|
||||
|
||||
// Read into multiple buffers
|
||||
let mut header_buf = vec![0u8; 6];
|
||||
let mut data_buf = vec![0u8; 22];
|
||||
let mut footer_buf = vec![0u8; 6];
|
||||
|
||||
let mut read_vecs = vec![
|
||||
IoVec::new_mut(&mut header_buf),
|
||||
IoVec::new_mut(&mut data_buf),
|
||||
IoVec::new_mut(&mut footer_buf),
|
||||
];
|
||||
|
||||
let bytes_read = vectored_reader.read_vectored(&mut read_vecs).await?;
|
||||
println!("Read {} bytes in vectored operation", bytes_read);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Async Stream Processing
|
||||
|
||||
```rust
|
||||
use rustfs_rio::{AsyncStreamProcessor, ProcessorChain};
|
||||
use futures::StreamExt;
|
||||
|
||||
async fn stream_processing_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create processor chain
|
||||
let processor = ProcessorChain::new()
|
||||
.add_processor(Box::new(CompressionProcessor::new(CompressionAlgorithm::Zstd)))
|
||||
.add_processor(Box::new(EncryptionProcessor::new(encryption_key)))
|
||||
.add_processor(Box::new(ChecksumProcessor::new(ChecksumAlgorithm::Crc32)));
|
||||
|
||||
// Create async stream processor
|
||||
let mut stream_processor = AsyncStreamProcessor::new(input_stream, processor);
|
||||
|
||||
// Process stream chunks
|
||||
while let Some(chunk) = stream_processor.next().await {
|
||||
let processed_chunk = chunk?;
|
||||
|
||||
// Handle processed chunk
|
||||
output_stream.write_all(&processed_chunk).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Rio Architecture
|
||||
|
||||
```
|
||||
Rio I/O Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Stream API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Encryption │ Compression │ Checksum │ Vectored I/O │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Buffer Management │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Zero-Copy │ Async I/O │ Backpressure Control │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Tokio Runtime Integration │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Performance Features
|
||||
|
||||
| Feature | Benefit | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Zero-Copy | Reduced memory usage | Direct buffer operations |
|
||||
| Async I/O | High concurrency | Tokio-based operations |
|
||||
| Vectored I/O | Reduced syscalls | Scatter-gather operations |
|
||||
| Buffer Pooling | Memory efficiency | Reusable buffer management |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test streaming operations
|
||||
cargo test streaming
|
||||
|
||||
# Test encryption
|
||||
cargo test encryption
|
||||
|
||||
# Test compression
|
||||
cargo test compression
|
||||
|
||||
# Run benchmarks
|
||||
cargo bench
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Dependencies**: Tokio async runtime
|
||||
- **Hardware**: AES-NI support recommended for encryption
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Utils](../utils) - Utility functions
|
||||
- [RustFS Crypto](../crypto) - Cryptographic operations
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Rio API Reference](https://docs.rustfs.com/rio/)
|
||||
- [Performance Guide](https://docs.rustfs.com/performance/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🚀 by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,591 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS S3Select API - SQL Query Interface
|
||||
|
||||
<p align="center">
|
||||
<strong>AWS S3 Select compatible SQL query API for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS S3Select API** provides AWS S3 Select compatible SQL query capabilities for the [RustFS](https://rustfs.com) distributed object storage system. It enables clients to retrieve subsets of data from objects using SQL expressions, reducing data transfer and improving query performance through server-side filtering.
|
||||
|
||||
> **Note:** This is a high-performance submodule of RustFS that provides essential SQL query capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 📊 SQL Query Support
|
||||
|
||||
- **Standard SQL**: Support for SELECT, WHERE, GROUP BY, ORDER BY clauses
|
||||
- **Data Types**: Support for strings, numbers, booleans, timestamps
|
||||
- **Functions**: Built-in SQL functions (aggregation, string, date functions)
|
||||
- **Complex Expressions**: Nested queries and complex conditional logic
|
||||
|
||||
### 📁 Format Support
|
||||
|
||||
- **CSV Files**: Comma-separated values with customizable delimiters
|
||||
- **JSON Documents**: JSON objects and arrays with path expressions
|
||||
- **Parquet Files**: Columnar format with schema evolution
|
||||
- **Apache Arrow**: High-performance columnar data format
|
||||
|
||||
### 🚀 Performance Features
|
||||
|
||||
- **Streaming Processing**: Process large files without loading into memory
|
||||
- **Parallel Execution**: Multi-threaded query execution
|
||||
- **Predicate Pushdown**: Push filters down to storage layer
|
||||
- **Columnar Processing**: Efficient columnar data processing with Apache DataFusion
|
||||
|
||||
### 🔧 S3 Compatibility
|
||||
|
||||
- **S3 Select API**: Full compatibility with AWS S3 Select API
|
||||
- **Request Formats**: Support for JSON and XML request formats
|
||||
- **Response Streaming**: Streaming query results back to clients
|
||||
- **Error Handling**: AWS-compatible error responses
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-s3select-api = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic S3 Select Query
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_api::{S3SelectService, SelectRequest, InputSerialization, OutputSerialization};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create S3 Select service
|
||||
let s3select = S3SelectService::new().await?;
|
||||
|
||||
// Configure input format (CSV)
|
||||
let input_serialization = InputSerialization::CSV {
|
||||
file_header_info: "USE".to_string(),
|
||||
record_delimiter: "\n".to_string(),
|
||||
field_delimiter: ",".to_string(),
|
||||
quote_character: "\"".to_string(),
|
||||
quote_escape_character: "\"".to_string(),
|
||||
comments: "#".to_string(),
|
||||
};
|
||||
|
||||
// Configure output format
|
||||
let output_serialization = OutputSerialization::JSON {
|
||||
record_delimiter: "\n".to_string(),
|
||||
};
|
||||
|
||||
// Create select request
|
||||
let select_request = SelectRequest {
|
||||
bucket: "sales-data".to_string(),
|
||||
key: "2024/sales.csv".to_string(),
|
||||
expression: "SELECT name, revenue FROM S3Object WHERE revenue > 10000".to_string(),
|
||||
expression_type: "SQL".to_string(),
|
||||
input_serialization,
|
||||
output_serialization,
|
||||
request_progress: false,
|
||||
};
|
||||
|
||||
// Execute query
|
||||
let mut result_stream = s3select.select_object_content(select_request).await?;
|
||||
|
||||
// Process streaming results
|
||||
while let Some(event) = result_stream.next().await {
|
||||
match event? {
|
||||
SelectEvent::Records(data) => {
|
||||
println!("Query result: {}", String::from_utf8(data)?);
|
||||
}
|
||||
SelectEvent::Stats(stats) => {
|
||||
println!("Bytes scanned: {}", stats.bytes_scanned);
|
||||
println!("Bytes processed: {}", stats.bytes_processed);
|
||||
println!("Bytes returned: {}", stats.bytes_returned);
|
||||
}
|
||||
SelectEvent::Progress(progress) => {
|
||||
println!("Progress: {}%", progress.details.bytes_processed_percent);
|
||||
}
|
||||
SelectEvent::End => {
|
||||
println!("Query completed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### CSV Data Processing
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_api::{S3SelectService, CSVInputSerialization};
|
||||
|
||||
async fn csv_processing_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let s3select = S3SelectService::new().await?;
|
||||
|
||||
// Configure CSV input with custom settings
|
||||
let csv_input = CSVInputSerialization {
|
||||
file_header_info: "USE".to_string(),
|
||||
record_delimiter: "\r\n".to_string(),
|
||||
field_delimiter: "|".to_string(),
|
||||
quote_character: "'".to_string(),
|
||||
quote_escape_character: "\\".to_string(),
|
||||
comments: "//".to_string(),
|
||||
allow_quoted_record_delimiter: false,
|
||||
};
|
||||
|
||||
// Query with aggregation
|
||||
let select_request = SelectRequest {
|
||||
bucket: "analytics".to_string(),
|
||||
key: "user-events.csv".to_string(),
|
||||
expression: r#"
|
||||
SELECT
|
||||
event_type,
|
||||
COUNT(*) as event_count,
|
||||
AVG(CAST(duration as DECIMAL)) as avg_duration
|
||||
FROM S3Object
|
||||
WHERE timestamp >= '2024-01-01'
|
||||
GROUP BY event_type
|
||||
ORDER BY event_count DESC
|
||||
"#.to_string(),
|
||||
expression_type: "SQL".to_string(),
|
||||
input_serialization: InputSerialization::CSV(csv_input),
|
||||
output_serialization: OutputSerialization::JSON {
|
||||
record_delimiter: "\n".to_string(),
|
||||
},
|
||||
request_progress: true,
|
||||
};
|
||||
|
||||
let mut result_stream = s3select.select_object_content(select_request).await?;
|
||||
|
||||
let mut total_events = 0;
|
||||
while let Some(event) = result_stream.next().await {
|
||||
match event? {
|
||||
SelectEvent::Records(data) => {
|
||||
let result: serde_json::Value = serde_json::from_slice(&data)?;
|
||||
println!("Event type: {}, Count: {}, Avg duration: {}",
|
||||
result["event_type"], result["event_count"], result["avg_duration"]);
|
||||
total_events += result["event_count"].as_u64().unwrap_or(0);
|
||||
}
|
||||
SelectEvent::Progress(progress) => {
|
||||
println!("Processing: {}%", progress.details.bytes_processed_percent);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Total events processed: {}", total_events);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### JSON Data Querying
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_api::{JSONInputSerialization, JSONType};
|
||||
|
||||
async fn json_querying_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let s3select = S3SelectService::new().await?;
|
||||
|
||||
// Configure JSON input
|
||||
let json_input = JSONInputSerialization {
|
||||
json_type: JSONType::Lines, // JSON Lines format
|
||||
};
|
||||
|
||||
// Query nested JSON data
|
||||
let select_request = SelectRequest {
|
||||
bucket: "logs".to_string(),
|
||||
key: "application.jsonl".to_string(),
|
||||
expression: r#"
|
||||
SELECT
|
||||
s.timestamp,
|
||||
s.level,
|
||||
s.message,
|
||||
s.metadata.user_id,
|
||||
s.metadata.request_id
|
||||
FROM S3Object[*] s
|
||||
WHERE s.level = 'ERROR'
|
||||
AND s.metadata.user_id IS NOT NULL
|
||||
ORDER BY s.timestamp DESC
|
||||
"#.to_string(),
|
||||
expression_type: "SQL".to_string(),
|
||||
input_serialization: InputSerialization::JSON(json_input),
|
||||
output_serialization: OutputSerialization::JSON {
|
||||
record_delimiter: "\n".to_string(),
|
||||
},
|
||||
request_progress: false,
|
||||
};
|
||||
|
||||
let mut result_stream = s3select.select_object_content(select_request).await?;
|
||||
|
||||
while let Some(event) = result_stream.next().await {
|
||||
if let SelectEvent::Records(data) = event? {
|
||||
let log_entry: serde_json::Value = serde_json::from_slice(&data)?;
|
||||
println!("Error at {}: {} (User: {}, Request: {})",
|
||||
log_entry["timestamp"],
|
||||
log_entry["message"],
|
||||
log_entry["user_id"],
|
||||
log_entry["request_id"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Parquet File Analysis
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_api::{ParquetInputSerialization};
|
||||
|
||||
async fn parquet_analysis_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let s3select = S3SelectService::new().await?;
|
||||
|
||||
// Parquet files don't need serialization configuration
|
||||
let parquet_input = ParquetInputSerialization {};
|
||||
|
||||
// Complex analytical query
|
||||
let select_request = SelectRequest {
|
||||
bucket: "data-warehouse".to_string(),
|
||||
key: "sales/2024/q1/sales_data.parquet".to_string(),
|
||||
expression: r#"
|
||||
SELECT
|
||||
region,
|
||||
product_category,
|
||||
SUM(amount) as total_sales,
|
||||
COUNT(*) as transaction_count,
|
||||
AVG(amount) as avg_transaction,
|
||||
MIN(amount) as min_sale,
|
||||
MAX(amount) as max_sale
|
||||
FROM S3Object
|
||||
WHERE sale_date >= '2024-01-01'
|
||||
AND sale_date < '2024-04-01'
|
||||
AND amount > 0
|
||||
GROUP BY region, product_category
|
||||
HAVING SUM(amount) > 50000
|
||||
ORDER BY total_sales DESC
|
||||
LIMIT 20
|
||||
"#.to_string(),
|
||||
expression_type: "SQL".to_string(),
|
||||
input_serialization: InputSerialization::Parquet(parquet_input),
|
||||
output_serialization: OutputSerialization::JSON {
|
||||
record_delimiter: "\n".to_string(),
|
||||
},
|
||||
request_progress: true,
|
||||
};
|
||||
|
||||
let mut result_stream = s3select.select_object_content(select_request).await?;
|
||||
|
||||
while let Some(event) = result_stream.next().await {
|
||||
match event? {
|
||||
SelectEvent::Records(data) => {
|
||||
let sales_data: serde_json::Value = serde_json::from_slice(&data)?;
|
||||
println!("Region: {}, Category: {}, Total Sales: ${:.2}",
|
||||
sales_data["region"],
|
||||
sales_data["product_category"],
|
||||
sales_data["total_sales"]
|
||||
);
|
||||
}
|
||||
SelectEvent::Stats(stats) => {
|
||||
println!("Query statistics:");
|
||||
println!(" Bytes scanned: {}", stats.bytes_scanned);
|
||||
println!(" Bytes processed: {}", stats.bytes_processed);
|
||||
println!(" Bytes returned: {}", stats.bytes_returned);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced SQL Functions
|
||||
|
||||
```rust
|
||||
async fn advanced_sql_functions_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let s3select = S3SelectService::new().await?;
|
||||
|
||||
// Query with various SQL functions
|
||||
let select_request = SelectRequest {
|
||||
bucket: "analytics".to_string(),
|
||||
key: "user_data.csv".to_string(),
|
||||
expression: r#"
|
||||
SELECT
|
||||
-- String functions
|
||||
UPPER(name) as name_upper,
|
||||
SUBSTRING(email, 1, POSITION('@' IN email) - 1) as username,
|
||||
LENGTH(description) as desc_length,
|
||||
|
||||
-- Date functions
|
||||
EXTRACT(YEAR FROM registration_date) as reg_year,
|
||||
DATE_DIFF('day', registration_date, last_login) as days_since_reg,
|
||||
|
||||
-- Numeric functions
|
||||
ROUND(score, 2) as rounded_score,
|
||||
CASE
|
||||
WHEN score >= 90 THEN 'Excellent'
|
||||
WHEN score >= 70 THEN 'Good'
|
||||
WHEN score >= 50 THEN 'Average'
|
||||
ELSE 'Poor'
|
||||
END as score_category,
|
||||
|
||||
-- Conditional logic
|
||||
COALESCE(nickname, SUBSTRING(name, 1, POSITION(' ' IN name) - 1)) as display_name
|
||||
|
||||
FROM S3Object
|
||||
WHERE registration_date IS NOT NULL
|
||||
AND score IS NOT NULL
|
||||
ORDER BY score DESC
|
||||
"#.to_string(),
|
||||
expression_type: "SQL".to_string(),
|
||||
input_serialization: InputSerialization::CSV {
|
||||
file_header_info: "USE".to_string(),
|
||||
record_delimiter: "\n".to_string(),
|
||||
field_delimiter: ",".to_string(),
|
||||
quote_character: "\"".to_string(),
|
||||
quote_escape_character: "\"".to_string(),
|
||||
comments: "#".to_string(),
|
||||
},
|
||||
output_serialization: OutputSerialization::JSON {
|
||||
record_delimiter: "\n".to_string(),
|
||||
},
|
||||
request_progress: false,
|
||||
};
|
||||
|
||||
let mut result_stream = s3select.select_object_content(select_request).await?;
|
||||
|
||||
while let Some(event) = result_stream.next().await {
|
||||
if let SelectEvent::Records(data) = event? {
|
||||
let user: serde_json::Value = serde_json::from_slice(&data)?;
|
||||
println!("User: {} ({}) - Score: {} ({})",
|
||||
user["display_name"],
|
||||
user["username"],
|
||||
user["rounded_score"],
|
||||
user["score_category"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Large Datasets
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_api::{SelectObjectContentStream, ProgressDetails};
|
||||
|
||||
async fn streaming_large_datasets() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let s3select = S3SelectService::new().await?;
|
||||
|
||||
let select_request = SelectRequest {
|
||||
bucket: "big-data".to_string(),
|
||||
key: "large_dataset.csv".to_string(),
|
||||
expression: "SELECT * FROM S3Object WHERE status = 'active'".to_string(),
|
||||
expression_type: "SQL".to_string(),
|
||||
input_serialization: InputSerialization::CSV {
|
||||
file_header_info: "USE".to_string(),
|
||||
record_delimiter: "\n".to_string(),
|
||||
field_delimiter: ",".to_string(),
|
||||
quote_character: "\"".to_string(),
|
||||
quote_escape_character: "\"".to_string(),
|
||||
comments: "".to_string(),
|
||||
},
|
||||
output_serialization: OutputSerialization::JSON {
|
||||
record_delimiter: "\n".to_string(),
|
||||
},
|
||||
request_progress: true,
|
||||
};
|
||||
|
||||
let mut result_stream = s3select.select_object_content(select_request).await?;
|
||||
|
||||
let mut processed_count = 0;
|
||||
let mut output_file = tokio::fs::File::create("filtered_results.jsonl").await?;
|
||||
|
||||
while let Some(event) = result_stream.next().await {
|
||||
match event? {
|
||||
SelectEvent::Records(data) => {
|
||||
// Write results to file
|
||||
output_file.write_all(&data).await?;
|
||||
processed_count += 1;
|
||||
|
||||
if processed_count % 1000 == 0 {
|
||||
println!("Processed {} records", processed_count);
|
||||
}
|
||||
}
|
||||
SelectEvent::Progress(progress) => {
|
||||
println!("Progress: {:.1}% ({} bytes processed)",
|
||||
progress.details.bytes_processed_percent,
|
||||
progress.details.bytes_processed
|
||||
);
|
||||
}
|
||||
SelectEvent::Stats(stats) => {
|
||||
println!("Final statistics:");
|
||||
println!(" Total bytes scanned: {}", stats.bytes_scanned);
|
||||
println!(" Total bytes processed: {}", stats.bytes_processed);
|
||||
println!(" Total bytes returned: {}", stats.bytes_returned);
|
||||
println!(" Processing efficiency: {:.2}%",
|
||||
(stats.bytes_returned as f64 / stats.bytes_scanned as f64) * 100.0
|
||||
);
|
||||
}
|
||||
SelectEvent::End => {
|
||||
println!("Streaming completed. Total records: {}", processed_count);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output_file.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP API Integration
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_api::{S3SelectHandler, SelectRequestXML};
|
||||
use axum::{Router, Json, extract::{Path, Query}};
|
||||
|
||||
async fn setup_s3select_http_api() -> Router {
|
||||
let s3select_handler = S3SelectHandler::new().await.unwrap();
|
||||
|
||||
Router::new()
|
||||
.route("/buckets/:bucket/objects/:key/select",
|
||||
axum::routing::post(handle_select_object_content))
|
||||
.layer(Extension(s3select_handler))
|
||||
}
|
||||
|
||||
async fn handle_select_object_content(
|
||||
Path((bucket, key)): Path<(String, String)>,
|
||||
Extension(handler): Extension<S3SelectHandler>,
|
||||
body: String,
|
||||
) -> Result<impl axum::response::IntoResponse, Box<dyn std::error::Error>> {
|
||||
// Parse S3 Select request (XML or JSON)
|
||||
let select_request = handler.parse_request(&body, &bucket, &key).await?;
|
||||
|
||||
// Execute query
|
||||
let result_stream = handler.execute_select(select_request).await?;
|
||||
|
||||
// Return streaming response
|
||||
let response = axum::response::Response::builder()
|
||||
.header("content-type", "application/xml")
|
||||
.header("x-amz-request-id", "12345")
|
||||
.body(axum::body::Body::from_stream(result_stream))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### S3Select API Architecture
|
||||
|
||||
```
|
||||
S3Select API Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ S3 Select HTTP API │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Request │ Response │ Streaming │ Error │
|
||||
│ Parsing │ Formatting │ Results │ Handling │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Query Engine (DataFusion) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ SQL Parser │ Optimizer │ Execution │ Streaming│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Storage Integration │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Supported Data Formats
|
||||
|
||||
| Format | Features | Use Cases |
|
||||
|--------|----------|-----------|
|
||||
| CSV | Custom delimiters, headers, quotes | Log files, exports |
|
||||
| JSON | Objects, arrays, nested data | APIs, documents |
|
||||
| JSON Lines | Streaming JSON records | Event logs, analytics |
|
||||
| Parquet | Columnar, schema evolution | Data warehousing |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test SQL parsing
|
||||
cargo test sql_parsing
|
||||
|
||||
# Test format support
|
||||
cargo test format_support
|
||||
|
||||
# Test streaming
|
||||
cargo test streaming
|
||||
|
||||
# Integration tests
|
||||
cargo test --test integration
|
||||
|
||||
# Performance tests
|
||||
cargo test --test performance --release
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Dependencies**: Apache DataFusion, Arrow
|
||||
- **Memory**: Sufficient RAM for query processing
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS S3Select Query](../s3select-query) - Query engine implementation
|
||||
- [RustFS ECStore](../ecstore) - Storage backend
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [S3Select API Reference](https://docs.rustfs.com/s3select-api/)
|
||||
- [SQL Reference](https://docs.rustfs.com/sql/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 📊 by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,657 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS S3Select Query - High-Performance Query Engine
|
||||
|
||||
<p align="center">
|
||||
<strong>Apache DataFusion-powered SQL query engine for RustFS S3 Select implementation</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS S3Select Query** is the high-performance query engine that powers SQL processing for the [RustFS](https://rustfs.com) S3 Select API. Built on Apache DataFusion, it provides blazing-fast SQL execution with advanced optimization techniques, streaming processing, and support for multiple data formats.
|
||||
|
||||
> **Note:** This is a core performance-critical submodule of RustFS that provides the SQL query execution engine for the S3 Select API. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🚀 High-Performance Query Engine
|
||||
|
||||
- **Apache DataFusion**: Built on the fastest SQL engine in Rust
|
||||
- **Vectorized Processing**: SIMD-accelerated columnar processing
|
||||
- **Parallel Execution**: Multi-threaded query execution
|
||||
- **Memory Efficient**: Streaming processing with minimal memory footprint
|
||||
|
||||
### 📊 Advanced SQL Support
|
||||
|
||||
- **Standard SQL**: Full support for SQL:2016 standard
|
||||
- **Complex Queries**: Joins, subqueries, window functions, CTEs
|
||||
- **Aggregations**: Group by, having, order by with optimizations
|
||||
- **Built-in Functions**: 200+ SQL functions including UDFs
|
||||
|
||||
### 🔧 Query Optimization
|
||||
|
||||
- **Cost-Based Optimizer**: Intelligent query planning
|
||||
- **Predicate Pushdown**: Push filters to data sources
|
||||
- **Projection Pushdown**: Only read required columns
|
||||
- **Join Optimization**: Hash joins, sort-merge joins
|
||||
|
||||
### 📁 Data Format Support
|
||||
|
||||
- **Parquet**: Native columnar format with predicate pushdown
|
||||
- **CSV**: Efficient CSV parsing with schema inference
|
||||
- **JSON**: Nested JSON processing with path expressions
|
||||
- **Arrow**: Zero-copy Arrow format processing
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-s3select-query = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Query Engine Setup
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_query::{QueryEngine, DataSource, QueryResult};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create query engine
|
||||
let query_engine = QueryEngine::new().await?;
|
||||
|
||||
// Register data source
|
||||
let data_source = DataSource::from_csv("s3://bucket/data.csv").await?;
|
||||
query_engine.register_table("sales", data_source).await?;
|
||||
|
||||
// Execute SQL query
|
||||
let sql = "SELECT region, SUM(amount) as total FROM sales GROUP BY region";
|
||||
let result = query_engine.execute_query(sql).await?;
|
||||
|
||||
// Process results
|
||||
while let Some(batch) = result.next().await {
|
||||
let batch = batch?;
|
||||
println!("Batch with {} rows", batch.num_rows());
|
||||
|
||||
// Convert to JSON for display
|
||||
let json_rows = batch.to_json()?;
|
||||
for row in json_rows {
|
||||
println!("{}", row);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Query Execution
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_query::{
|
||||
QueryEngine, QueryPlan, ExecutionConfig,
|
||||
DataSource, SchemaRef, RecordBatch
|
||||
};
|
||||
|
||||
async fn advanced_query_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Configure execution settings
|
||||
let config = ExecutionConfig::new()
|
||||
.with_target_partitions(8)
|
||||
.with_batch_size(8192)
|
||||
.with_max_memory(1024 * 1024 * 1024); // 1GB memory limit
|
||||
|
||||
let query_engine = QueryEngine::with_config(config).await?;
|
||||
|
||||
// Register multiple data sources
|
||||
let customers = DataSource::from_parquet("s3://warehouse/customers.parquet").await?;
|
||||
let orders = DataSource::from_csv("s3://logs/orders.csv").await?;
|
||||
let products = DataSource::from_json("s3://catalog/products.json").await?;
|
||||
|
||||
query_engine.register_table("customers", customers).await?;
|
||||
query_engine.register_table("orders", orders).await?;
|
||||
query_engine.register_table("products", products).await?;
|
||||
|
||||
// Complex analytical query
|
||||
let sql = r#"
|
||||
SELECT
|
||||
c.customer_segment,
|
||||
p.category,
|
||||
COUNT(*) as order_count,
|
||||
SUM(o.amount) as total_revenue,
|
||||
AVG(o.amount) as avg_order_value,
|
||||
STDDEV(o.amount) as revenue_stddev
|
||||
FROM customers c
|
||||
JOIN orders o ON c.customer_id = o.customer_id
|
||||
JOIN products p ON o.product_id = p.product_id
|
||||
WHERE o.order_date >= '2024-01-01'
|
||||
AND o.status = 'completed'
|
||||
GROUP BY c.customer_segment, p.category
|
||||
HAVING SUM(o.amount) > 10000
|
||||
ORDER BY total_revenue DESC
|
||||
LIMIT 50
|
||||
"#;
|
||||
|
||||
// Get query plan for optimization analysis
|
||||
let plan = query_engine.create_logical_plan(sql).await?;
|
||||
println!("Query plan:\n{}", plan.display_indent());
|
||||
|
||||
// Execute with streaming results
|
||||
let mut result_stream = query_engine.execute_stream(sql).await?;
|
||||
|
||||
let mut total_rows = 0;
|
||||
while let Some(batch) = result_stream.next().await {
|
||||
let batch = batch?;
|
||||
total_rows += batch.num_rows();
|
||||
|
||||
// Process batch
|
||||
for row_idx in 0..batch.num_rows() {
|
||||
let segment = batch.column_by_name("customer_segment")?
|
||||
.as_any().downcast_ref::<StringArray>()
|
||||
.unwrap().value(row_idx);
|
||||
let category = batch.column_by_name("category")?
|
||||
.as_any().downcast_ref::<StringArray>()
|
||||
.unwrap().value(row_idx);
|
||||
let revenue = batch.column_by_name("total_revenue")?
|
||||
.as_any().downcast_ref::<Float64Array>()
|
||||
.unwrap().value(row_idx);
|
||||
|
||||
println!("Segment: {}, Category: {}, Revenue: ${:.2}",
|
||||
segment, category, revenue);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Total rows processed: {}", total_rows);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Data Sources
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_query::{DataSource, TableProvider, SchemaRef};
|
||||
use datafusion::arrow::datatypes::{Schema, Field, DataType};
|
||||
use datafusion::arrow::record_batch::RecordBatch;
|
||||
|
||||
struct CustomS3DataSource {
|
||||
bucket: String,
|
||||
key: String,
|
||||
schema: SchemaRef,
|
||||
}
|
||||
|
||||
impl CustomS3DataSource {
|
||||
async fn new(bucket: &str, key: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
// Infer schema from S3 object
|
||||
let schema = Self::infer_schema(bucket, key).await?;
|
||||
|
||||
Ok(Self {
|
||||
bucket: bucket.to_string(),
|
||||
key: key.to_string(),
|
||||
schema: Arc::new(schema),
|
||||
})
|
||||
}
|
||||
|
||||
async fn infer_schema(bucket: &str, key: &str) -> Result<Schema, Box<dyn std::error::Error>> {
|
||||
// Read sample data to infer schema
|
||||
let sample_data = read_s3_sample(bucket, key).await?;
|
||||
|
||||
// Create schema based on data format
|
||||
let schema = Schema::new(vec![
|
||||
Field::new("id", DataType::Int64, false),
|
||||
Field::new("name", DataType::Utf8, false),
|
||||
Field::new("value", DataType::Float64, true),
|
||||
Field::new("timestamp", DataType::Timestamp(TimeUnit::Millisecond, None), false),
|
||||
]);
|
||||
|
||||
Ok(schema)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TableProvider for CustomS3DataSource {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn schema(&self) -> SchemaRef {
|
||||
self.schema.clone()
|
||||
}
|
||||
|
||||
async fn scan(
|
||||
&self,
|
||||
projection: Option<&Vec<usize>>,
|
||||
filters: &[Expr],
|
||||
limit: Option<usize>,
|
||||
) -> Result<Arc<dyn ExecutionPlan>, DataFusionError> {
|
||||
// Create execution plan for scanning S3 data
|
||||
let scan_plan = S3ScanExec::new(
|
||||
self.bucket.clone(),
|
||||
self.key.clone(),
|
||||
self.schema.clone(),
|
||||
projection.cloned(),
|
||||
filters.to_vec(),
|
||||
limit,
|
||||
);
|
||||
|
||||
Ok(Arc::new(scan_plan))
|
||||
}
|
||||
}
|
||||
|
||||
async fn custom_data_source_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let query_engine = QueryEngine::new().await?;
|
||||
|
||||
// Register custom data source
|
||||
let custom_source = CustomS3DataSource::new("analytics", "events.parquet").await?;
|
||||
query_engine.register_table("events", Arc::new(custom_source)).await?;
|
||||
|
||||
// Query custom data source
|
||||
let sql = "SELECT * FROM events WHERE timestamp > NOW() - INTERVAL '1 day'";
|
||||
let result = query_engine.execute_query(sql).await?;
|
||||
|
||||
// Process results
|
||||
while let Some(batch) = result.next().await {
|
||||
let batch = batch?;
|
||||
println!("Custom source batch: {} rows", batch.num_rows());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Query Optimization and Analysis
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_query::{QueryEngine, QueryOptimizer, QueryMetrics};
|
||||
|
||||
async fn query_optimization_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let query_engine = QueryEngine::new().await?;
|
||||
|
||||
// Register data source
|
||||
let data_source = DataSource::from_parquet("s3://warehouse/sales.parquet").await?;
|
||||
query_engine.register_table("sales", data_source).await?;
|
||||
|
||||
let sql = r#"
|
||||
SELECT
|
||||
region,
|
||||
product_category,
|
||||
SUM(amount) as total_sales,
|
||||
COUNT(*) as transaction_count
|
||||
FROM sales
|
||||
WHERE sale_date >= '2024-01-01'
|
||||
AND amount > 100
|
||||
GROUP BY region, product_category
|
||||
ORDER BY total_sales DESC
|
||||
"#;
|
||||
|
||||
// Analyze query plan
|
||||
let logical_plan = query_engine.create_logical_plan(sql).await?;
|
||||
println!("Logical Plan:\n{}", logical_plan.display_indent());
|
||||
|
||||
let physical_plan = query_engine.create_physical_plan(&logical_plan).await?;
|
||||
println!("Physical Plan:\n{}", physical_plan.display_indent());
|
||||
|
||||
// Execute with metrics
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut result_stream = query_engine.execute_stream(sql).await?;
|
||||
|
||||
let mut total_rows = 0;
|
||||
let mut total_batches = 0;
|
||||
|
||||
while let Some(batch) = result_stream.next().await {
|
||||
let batch = batch?;
|
||||
total_rows += batch.num_rows();
|
||||
total_batches += 1;
|
||||
}
|
||||
|
||||
let execution_time = start_time.elapsed();
|
||||
|
||||
// Get execution metrics
|
||||
let metrics = query_engine.get_execution_metrics().await?;
|
||||
|
||||
println!("Query Performance:");
|
||||
println!(" Execution time: {:?}", execution_time);
|
||||
println!(" Total rows: {}", total_rows);
|
||||
println!(" Total batches: {}", total_batches);
|
||||
println!(" Rows per second: {:.2}", total_rows as f64 / execution_time.as_secs_f64());
|
||||
println!(" Memory used: {} bytes", metrics.memory_used);
|
||||
println!(" Bytes scanned: {}", metrics.bytes_scanned);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Query Processing
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_query::{StreamingQueryEngine, StreamingResult};
|
||||
use futures::StreamExt;
|
||||
|
||||
async fn streaming_processing_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let streaming_engine = StreamingQueryEngine::new().await?;
|
||||
|
||||
// Register streaming data source
|
||||
let stream_source = DataSource::from_streaming_csv("s3://logs/stream.csv").await?;
|
||||
streaming_engine.register_table("log_stream", stream_source).await?;
|
||||
|
||||
// Continuous query with windowing
|
||||
let sql = r#"
|
||||
SELECT
|
||||
TUMBLE_START(timestamp, INTERVAL '5' MINUTE) as window_start,
|
||||
COUNT(*) as event_count,
|
||||
AVG(response_time) as avg_response_time,
|
||||
MAX(response_time) as max_response_time
|
||||
FROM log_stream
|
||||
WHERE status_code >= 400
|
||||
GROUP BY TUMBLE(timestamp, INTERVAL '5' MINUTE)
|
||||
"#;
|
||||
|
||||
let mut result_stream = streaming_engine.execute_streaming_query(sql).await?;
|
||||
|
||||
// Process streaming results
|
||||
while let Some(window_result) = result_stream.next().await {
|
||||
let batch = window_result?;
|
||||
|
||||
for row_idx in 0..batch.num_rows() {
|
||||
let window_start = batch.column_by_name("window_start")?
|
||||
.as_any().downcast_ref::<TimestampArray>()
|
||||
.unwrap().value(row_idx);
|
||||
let event_count = batch.column_by_name("event_count")?
|
||||
.as_any().downcast_ref::<Int64Array>()
|
||||
.unwrap().value(row_idx);
|
||||
let avg_response = batch.column_by_name("avg_response_time")?
|
||||
.as_any().downcast_ref::<Float64Array>()
|
||||
.unwrap().value(row_idx);
|
||||
|
||||
println!("Window {}: {} errors, avg response time: {:.2}ms",
|
||||
window_start, event_count, avg_response);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### User-Defined Functions (UDFs)
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_query::{QueryEngine, ScalarUDF, Volatility};
|
||||
use datafusion::arrow::datatypes::{DataType, Field};
|
||||
|
||||
async fn custom_functions_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let query_engine = QueryEngine::new().await?;
|
||||
|
||||
// Register custom scalar function
|
||||
let extract_domain_udf = ScalarUDF::new(
|
||||
"extract_domain",
|
||||
vec![DataType::Utf8],
|
||||
DataType::Utf8,
|
||||
Volatility::Immutable,
|
||||
Arc::new(|args: &[ArrayRef]| {
|
||||
let emails = args[0].as_any().downcast_ref::<StringArray>().unwrap();
|
||||
let mut domains = Vec::new();
|
||||
|
||||
for i in 0..emails.len() {
|
||||
if let Some(email) = emails.value_opt(i) {
|
||||
if let Some(domain) = email.split('@').nth(1) {
|
||||
domains.push(Some(domain.to_string()));
|
||||
} else {
|
||||
domains.push(None);
|
||||
}
|
||||
} else {
|
||||
domains.push(None);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Arc::new(StringArray::from(domains)))
|
||||
}),
|
||||
);
|
||||
|
||||
query_engine.register_udf(extract_domain_udf).await?;
|
||||
|
||||
// Register aggregate function
|
||||
let percentile_udf = AggregateUDF::new(
|
||||
"percentile_90",
|
||||
vec![DataType::Float64],
|
||||
DataType::Float64,
|
||||
Volatility::Immutable,
|
||||
Arc::new(|| Box::new(PercentileAccumulator::new(0.9))),
|
||||
);
|
||||
|
||||
query_engine.register_udaf(percentile_udf).await?;
|
||||
|
||||
// Use custom functions in query
|
||||
let data_source = DataSource::from_csv("s3://users/profiles.csv").await?;
|
||||
query_engine.register_table("users", data_source).await?;
|
||||
|
||||
let sql = r#"
|
||||
SELECT
|
||||
extract_domain(email) as domain,
|
||||
COUNT(*) as user_count,
|
||||
percentile_90(score) as p90_score
|
||||
FROM users
|
||||
GROUP BY extract_domain(email)
|
||||
ORDER BY user_count DESC
|
||||
"#;
|
||||
|
||||
let result = query_engine.execute_query(sql).await?;
|
||||
|
||||
while let Some(batch) = result.next().await {
|
||||
let batch = batch?;
|
||||
|
||||
for row_idx in 0..batch.num_rows() {
|
||||
let domain = batch.column_by_name("domain")?
|
||||
.as_any().downcast_ref::<StringArray>()
|
||||
.unwrap().value(row_idx);
|
||||
let user_count = batch.column_by_name("user_count")?
|
||||
.as_any().downcast_ref::<Int64Array>()
|
||||
.unwrap().value(row_idx);
|
||||
let p90_score = batch.column_by_name("p90_score")?
|
||||
.as_any().downcast_ref::<Float64Array>()
|
||||
.unwrap().value(row_idx);
|
||||
|
||||
println!("Domain: {}, Users: {}, P90 Score: {:.2}",
|
||||
domain, user_count, p90_score);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Query Caching and Materialization
|
||||
|
||||
```rust
|
||||
use rustfs_s3select_query::{QueryEngine, QueryCache, MaterializedView};
|
||||
|
||||
async fn query_caching_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut query_engine = QueryEngine::new().await?;
|
||||
|
||||
// Enable query result caching
|
||||
let cache_config = QueryCache::new()
|
||||
.with_max_size(1024 * 1024 * 1024) // 1GB cache
|
||||
.with_ttl(Duration::from_secs(300)); // 5 minutes TTL
|
||||
|
||||
query_engine.enable_caching(cache_config).await?;
|
||||
|
||||
// Register data source
|
||||
let data_source = DataSource::from_parquet("s3://warehouse/transactions.parquet").await?;
|
||||
query_engine.register_table("transactions", data_source).await?;
|
||||
|
||||
// Create materialized view for common queries
|
||||
let materialized_view = MaterializedView::new(
|
||||
"daily_sales",
|
||||
r#"
|
||||
SELECT
|
||||
DATE(transaction_date) as date,
|
||||
SUM(amount) as total_sales,
|
||||
COUNT(*) as transaction_count
|
||||
FROM transactions
|
||||
GROUP BY DATE(transaction_date)
|
||||
"#.to_string(),
|
||||
Duration::from_secs(3600), // Refresh every hour
|
||||
);
|
||||
|
||||
query_engine.register_materialized_view(materialized_view).await?;
|
||||
|
||||
// Query using materialized view
|
||||
let sql = r#"
|
||||
SELECT
|
||||
date,
|
||||
total_sales,
|
||||
LAG(total_sales, 1) OVER (ORDER BY date) as prev_day_sales,
|
||||
(total_sales - LAG(total_sales, 1) OVER (ORDER BY date)) /
|
||||
LAG(total_sales, 1) OVER (ORDER BY date) * 100 as growth_rate
|
||||
FROM daily_sales
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '30' DAY
|
||||
ORDER BY date DESC
|
||||
"#;
|
||||
|
||||
// First execution - cache miss
|
||||
let start_time = std::time::Instant::now();
|
||||
let result1 = query_engine.execute_query(sql).await?;
|
||||
let mut rows1 = 0;
|
||||
while let Some(batch) = result1.next().await {
|
||||
rows1 += batch?.num_rows();
|
||||
}
|
||||
let first_execution_time = start_time.elapsed();
|
||||
|
||||
// Second execution - cache hit
|
||||
let start_time = std::time::Instant::now();
|
||||
let result2 = query_engine.execute_query(sql).await?;
|
||||
let mut rows2 = 0;
|
||||
while let Some(batch) = result2.next().await {
|
||||
rows2 += batch?.num_rows();
|
||||
}
|
||||
let second_execution_time = start_time.elapsed();
|
||||
|
||||
println!("First execution: {:?} ({} rows)", first_execution_time, rows1);
|
||||
println!("Second execution: {:?} ({} rows)", second_execution_time, rows2);
|
||||
println!("Cache speedup: {:.2}x",
|
||||
first_execution_time.as_secs_f64() / second_execution_time.as_secs_f64());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Query Engine Architecture
|
||||
|
||||
```
|
||||
Query Engine Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SQL Query Interface │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Parser │ Planner │ Optimizer │ Executor │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Apache DataFusion Core │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Vectorized │ Parallel │ Streaming │ Memory │
|
||||
│ Processing │ Execution │ Engine │ Management│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Data Source Integration │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Parquet │ CSV │ JSON │ Arrow │
|
||||
│ Reader │ Parser │ Parser │ Format │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Execution Flow
|
||||
|
||||
1. **SQL Parsing**: Convert SQL string to logical plan
|
||||
2. **Logical Optimization**: Apply rule-based optimizations
|
||||
3. **Physical Planning**: Create physical execution plan
|
||||
4. **Execution**: Execute plan with streaming results
|
||||
5. **Result Streaming**: Return results as Arrow batches
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test query execution
|
||||
cargo test query_execution
|
||||
|
||||
# Test optimization
|
||||
cargo test optimization
|
||||
|
||||
# Test data formats
|
||||
cargo test data_formats
|
||||
|
||||
# Benchmark tests
|
||||
cargo test --test benchmarks --release
|
||||
|
||||
# Integration tests
|
||||
cargo test --test integration
|
||||
```
|
||||
|
||||
## 📊 Performance Benchmarks
|
||||
|
||||
| Operation | Throughput | Latency | Memory |
|
||||
|-----------|------------|---------|---------|
|
||||
| CSV Scan | 2.5 GB/s | 10ms | 50MB |
|
||||
| Parquet Scan | 5.0 GB/s | 5ms | 30MB |
|
||||
| JSON Parse | 1.2 GB/s | 15ms | 80MB |
|
||||
| Aggregation | 1.8 GB/s | 20ms | 100MB |
|
||||
| Join | 800 MB/s | 50ms | 200MB |
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **CPU**: Multi-core recommended for parallel processing
|
||||
- **Memory**: Variable based on query complexity
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS S3Select API](../s3select-api) - S3 Select API implementation
|
||||
- [RustFS ECStore](../ecstore) - Storage backend
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [S3Select Query Reference](https://docs.rustfs.com/s3select-query/)
|
||||
- [DataFusion Integration Guide](https://docs.rustfs.com/datafusion/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with ⚡ by the RustFS Team
|
||||
</p>
|
||||
@@ -27,10 +27,14 @@ bytes = { workspace = true }
|
||||
http.workspace = true
|
||||
time.workspace = true
|
||||
hyper.workspace = true
|
||||
serde.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
rustfs-utils = { workspace = true, features = ["full"] }
|
||||
s3s.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Signer - Request Signing & Authentication
|
||||
|
||||
<p align="center">
|
||||
<strong>AWS-compatible request signing and authentication for RustFS object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Signer** provides AWS-compatible request signing and authentication for the [RustFS](https://rustfs.com) distributed object storage system. It implements AWS Signature Version 4 (SigV4) signing algorithm, pre-signed URLs, and various authentication methods to ensure secure API access.
|
||||
|
||||
> **Note:** This is a security-critical submodule of RustFS that provides essential authentication capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔐 AWS-Compatible Signing
|
||||
|
||||
- **SigV4 Implementation**: Full AWS Signature Version 4 support
|
||||
- **Pre-signed URLs**: Temporary access URLs with expiration
|
||||
- **Chunked Upload**: Streaming upload with signature validation
|
||||
- **Multi-part Upload**: Signature validation for large files
|
||||
|
||||
### 🛡️ Authentication Methods
|
||||
|
||||
- **Access Key/Secret**: Traditional AWS-style authentication
|
||||
- **STS Token**: Temporary security token support
|
||||
- **IAM Role**: Role-based authentication
|
||||
- **Anonymous Access**: Public read access support
|
||||
|
||||
### 🚀 Performance Features
|
||||
|
||||
- **Signature Caching**: Avoid repeated signature calculations
|
||||
- **Batch Signing**: Sign multiple requests efficiently
|
||||
- **Streaming Support**: Sign data streams without buffering
|
||||
- **Hardware Acceleration**: Use hardware crypto when available
|
||||
|
||||
### 🔧 Advanced Features
|
||||
|
||||
- **Custom Headers**: Support for custom and vendor headers
|
||||
- **Regional Signing**: Multi-region signature support
|
||||
- **Clock Skew Handling**: Automatic time synchronization
|
||||
- **Signature Validation**: Server-side signature verification
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-signer = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Request Signing
|
||||
|
||||
```rust
|
||||
use rustfs_signer::{Signer, SigningConfig, Credentials};
|
||||
use http::{Request, Method};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create credentials
|
||||
let credentials = Credentials::new(
|
||||
"AKIAIOSFODNN7EXAMPLE".to_string(),
|
||||
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
|
||||
None, // No session token
|
||||
);
|
||||
|
||||
// Create signing configuration
|
||||
let config = SigningConfig {
|
||||
region: "us-east-1".to_string(),
|
||||
service: "s3".to_string(),
|
||||
credentials,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Create signer
|
||||
let signer = Signer::new(config);
|
||||
|
||||
// Create request
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("https://example-bucket.s3.amazonaws.com/example-object")
|
||||
.body(Vec::new())?;
|
||||
|
||||
// Sign request
|
||||
let signed_request = signer.sign_request(request).await?;
|
||||
|
||||
println!("Authorization header: {:?}", signed_request.headers().get("authorization"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-signed URLs
|
||||
|
||||
```rust
|
||||
use rustfs_signer::{Signer, PresignedUrlRequest};
|
||||
use std::time::Duration;
|
||||
|
||||
async fn presigned_url_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let signer = Signer::new(signing_config);
|
||||
|
||||
// Create pre-signed URL for GET request
|
||||
let presigned_request = PresignedUrlRequest {
|
||||
method: Method::GET,
|
||||
uri: "https://example-bucket.s3.amazonaws.com/example-object".parse()?,
|
||||
headers: Default::default(),
|
||||
expires_in: Duration::from_secs(3600), // 1 hour
|
||||
};
|
||||
|
||||
let presigned_url = signer.presign_url(presigned_request).await?;
|
||||
println!("Pre-signed URL: {}", presigned_url);
|
||||
|
||||
// Create pre-signed URL for PUT request
|
||||
let put_request = PresignedUrlRequest {
|
||||
method: Method::PUT,
|
||||
uri: "https://example-bucket.s3.amazonaws.com/upload-object".parse()?,
|
||||
headers: {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("content-type", "text/plain".parse()?);
|
||||
headers
|
||||
},
|
||||
expires_in: Duration::from_secs(1800), // 30 minutes
|
||||
};
|
||||
|
||||
let upload_url = signer.presign_url(put_request).await?;
|
||||
println!("Pre-signed upload URL: {}", upload_url);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Upload Signing
|
||||
|
||||
```rust
|
||||
use rustfs_signer::{StreamingSigner, ChunkedUploadSigner};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
async fn streaming_upload_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let signer = Signer::new(signing_config);
|
||||
|
||||
// Create streaming signer
|
||||
let streaming_signer = StreamingSigner::new(signer, "s3".to_string());
|
||||
|
||||
// Create chunked upload signer
|
||||
let mut chunked_signer = ChunkedUploadSigner::new(
|
||||
streaming_signer,
|
||||
"example-bucket".to_string(),
|
||||
"large-file.dat".to_string(),
|
||||
);
|
||||
|
||||
// Initialize multipart upload
|
||||
let upload_id = chunked_signer.initiate_multipart_upload().await?;
|
||||
println!("Upload ID: {}", upload_id);
|
||||
|
||||
// Upload chunks
|
||||
let mut file = tokio::fs::File::open("large-file.dat").await?;
|
||||
let mut chunk_buffer = vec![0u8; 5 * 1024 * 1024]; // 5MB chunks
|
||||
let mut part_number = 1;
|
||||
let mut etags = Vec::new();
|
||||
|
||||
loop {
|
||||
let bytes_read = file.read(&mut chunk_buffer).await?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let chunk = &chunk_buffer[..bytes_read];
|
||||
let etag = chunked_signer.upload_part(part_number, chunk).await?;
|
||||
etags.push((part_number, etag));
|
||||
|
||||
part_number += 1;
|
||||
}
|
||||
|
||||
// Complete multipart upload
|
||||
chunked_signer.complete_multipart_upload(upload_id, etags).await?;
|
||||
println!("Upload completed successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Signature Validation
|
||||
|
||||
```rust
|
||||
use rustfs_signer::{SignatureValidator, ValidationResult};
|
||||
use http::HeaderMap;
|
||||
|
||||
async fn signature_validation_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let validator = SignatureValidator::new(signing_config);
|
||||
|
||||
// Extract signature from request headers
|
||||
let headers = HeaderMap::new(); // Headers from incoming request
|
||||
let method = "GET";
|
||||
let uri = "/example-bucket/example-object";
|
||||
let body = b""; // Request body
|
||||
|
||||
// Validate signature
|
||||
let validation_result = validator.validate_signature(
|
||||
method,
|
||||
uri,
|
||||
&headers,
|
||||
body,
|
||||
).await?;
|
||||
|
||||
match validation_result {
|
||||
ValidationResult::Valid { credentials, .. } => {
|
||||
println!("Signature valid for user: {}", credentials.access_key);
|
||||
}
|
||||
ValidationResult::Invalid { reason } => {
|
||||
println!("Signature invalid: {}", reason);
|
||||
}
|
||||
ValidationResult::Expired { expired_at } => {
|
||||
println!("Signature expired at: {}", expired_at);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Signing
|
||||
|
||||
```rust
|
||||
use rustfs_signer::{BatchSigner, BatchSigningRequest};
|
||||
|
||||
async fn batch_signing_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let signer = Signer::new(signing_config);
|
||||
let batch_signer = BatchSigner::new(signer);
|
||||
|
||||
// Create multiple requests
|
||||
let requests = vec![
|
||||
BatchSigningRequest {
|
||||
method: Method::GET,
|
||||
uri: "https://bucket1.s3.amazonaws.com/object1".parse()?,
|
||||
headers: HeaderMap::new(),
|
||||
body: Vec::new(),
|
||||
},
|
||||
BatchSigningRequest {
|
||||
method: Method::PUT,
|
||||
uri: "https://bucket2.s3.amazonaws.com/object2".parse()?,
|
||||
headers: HeaderMap::new(),
|
||||
body: b"Hello, World!".to_vec(),
|
||||
},
|
||||
];
|
||||
|
||||
// Sign all requests in batch
|
||||
let signed_requests = batch_signer.sign_batch(requests).await?;
|
||||
|
||||
for (i, signed_request) in signed_requests.iter().enumerate() {
|
||||
println!("Request {}: {:?}", i + 1, signed_request.headers().get("authorization"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Authentication
|
||||
|
||||
```rust
|
||||
use rustfs_signer::{CustomAuthenticator, AuthenticationResult};
|
||||
use async_trait::async_trait;
|
||||
|
||||
struct CustomAuth {
|
||||
// Custom authentication logic
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CustomAuthenticator for CustomAuth {
|
||||
async fn authenticate(&self, request: &Request<Vec<u8>>) -> Result<AuthenticationResult, Box<dyn std::error::Error>> {
|
||||
// Custom authentication logic
|
||||
let auth_header = request.headers().get("authorization");
|
||||
|
||||
if let Some(auth) = auth_header {
|
||||
// Parse and validate custom authentication
|
||||
let auth_str = auth.to_str()?;
|
||||
|
||||
if auth_str.starts_with("Custom ") {
|
||||
// Validate custom token
|
||||
let token = &auth_str[7..];
|
||||
if self.validate_token(token).await? {
|
||||
return Ok(AuthenticationResult::Authenticated {
|
||||
user_id: "custom-user".to_string(),
|
||||
permissions: vec!["read".to_string(), "write".to_string()],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AuthenticationResult::Unauthenticated)
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomAuth {
|
||||
async fn validate_token(&self, token: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
// Implement token validation logic
|
||||
Ok(token.len() > 10) // Simple example
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Signer Architecture
|
||||
|
||||
```
|
||||
Signer Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Signing API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ SigV4 │ Pre-signed │ Streaming │ Batch │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Signature Calculation │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ HMAC-SHA256 │ Canonicalization │ String to Sign │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Cryptographic Primitives │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Signing Process
|
||||
|
||||
| Step | Description | Purpose |
|
||||
|------|-------------|---------|
|
||||
| 1. Canonicalize | Format request components | Consistent representation |
|
||||
| 2. Create String to Sign | Combine canonicalized data | Prepare for signing |
|
||||
| 3. Calculate Signature | HMAC-SHA256 computation | Generate signature |
|
||||
| 4. Add Headers | Add signature to request | Complete authentication |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test signature generation
|
||||
cargo test signature
|
||||
|
||||
# Test pre-signed URLs
|
||||
cargo test presigned
|
||||
|
||||
# Test validation
|
||||
cargo test validation
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Dependencies**: Cryptographic libraries (ring, rustls)
|
||||
- **Compatibility**: AWS S3 API compatible
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS IAM](../iam) - Identity and access management
|
||||
- [RustFS Crypto](../crypto) - Cryptographic operations
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Signer API Reference](https://docs.rustfs.com/signer/)
|
||||
- [AWS S3 Compatibility](https://docs.rustfs.com/s3-compatibility/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🔐 by the RustFS Team
|
||||
</p>
|
||||
@@ -19,6 +19,7 @@ use time::{OffsetDateTime, macros::format_description};
|
||||
|
||||
use super::request_signature_v4::{SERVICE_TYPE_S3, get_scope, get_signature, get_signing_key};
|
||||
use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH;
|
||||
use s3s::Body;
|
||||
|
||||
const STREAMING_SIGN_ALGORITHM: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
|
||||
const STREAMING_SIGN_TRAILER_ALGORITHM: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER";
|
||||
@@ -68,7 +69,7 @@ fn _build_chunk_signature(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn streaming_sign_v4(
|
||||
mut req: request::Builder,
|
||||
mut req: request::Request<Body>,
|
||||
_access_key_id: &str,
|
||||
_secret_access_key: &str,
|
||||
session_token: &str,
|
||||
@@ -76,8 +77,8 @@ pub fn streaming_sign_v4(
|
||||
data_len: i64,
|
||||
req_time: OffsetDateTime, /*, sh256: md5simd::Hasher*/
|
||||
trailer: HeaderMap,
|
||||
) -> request::Builder {
|
||||
let headers = req.headers_mut().expect("err");
|
||||
) -> request::Request<Body> {
|
||||
let headers = req.headers_mut();
|
||||
|
||||
if trailer.is_empty() {
|
||||
headers.append("X-Amz-Content-Sha256", HeaderValue::from_str(STREAMING_SIGN_ALGORITHM).expect("err"));
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
use http::{HeaderValue, request};
|
||||
use time::{OffsetDateTime, macros::format_description};
|
||||
|
||||
use s3s::Body;
|
||||
|
||||
pub fn streaming_unsigned_v4(
|
||||
mut req: request::Builder,
|
||||
mut req: request::Request<Body>,
|
||||
session_token: &str,
|
||||
_data_len: i64,
|
||||
req_time: OffsetDateTime,
|
||||
) -> request::Builder {
|
||||
let headers = req.headers_mut().expect("err");
|
||||
) -> request::Request<Body> {
|
||||
let headers = req.headers_mut();
|
||||
|
||||
let chunked_value = HeaderValue::from_str(&["aws-chunked"].join(",")).expect("err");
|
||||
headers.insert(http::header::TRANSFER_ENCODING, chunked_value);
|
||||
|
||||
@@ -21,23 +21,22 @@ use time::{OffsetDateTime, format_description};
|
||||
|
||||
use super::utils::get_host_addr;
|
||||
use rustfs_utils::crypto::{base64_encode, hex, hmac_sha1};
|
||||
use s3s::Body;
|
||||
|
||||
const _SIGN_V4_ALGORITHM: &str = "AWS4-HMAC-SHA256";
|
||||
const SIGN_V2_ALGORITHM: &str = "AWS";
|
||||
|
||||
fn encode_url2path(req: &request::Builder, _virtual_host: bool) -> String {
|
||||
//path = serde_urlencoded::to_string(req.uri_ref().unwrap().path().unwrap()).unwrap();
|
||||
|
||||
req.uri_ref().unwrap().path().to_string()
|
||||
fn encode_url2path(req: &request::Request<Body>, _virtual_host: bool) -> String {
|
||||
req.uri().path().to_string()
|
||||
}
|
||||
|
||||
pub fn pre_sign_v2(
|
||||
mut req: request::Builder,
|
||||
mut req: request::Request<Body>,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
expires: i64,
|
||||
virtual_host: bool,
|
||||
) -> request::Builder {
|
||||
) -> request::Request<Body> {
|
||||
if access_key_id.is_empty() || secret_access_key.is_empty() {
|
||||
return req;
|
||||
}
|
||||
@@ -46,7 +45,7 @@ pub fn pre_sign_v2(
|
||||
let d = d.replace_time(time::Time::from_hms(0, 0, 0).unwrap());
|
||||
let epoch_expires = d.unix_timestamp() + expires;
|
||||
|
||||
let headers = req.headers_mut().expect("headers_mut err");
|
||||
let headers = req.headers_mut();
|
||||
let expires_str = headers.get("Expires");
|
||||
if expires_str.is_none() {
|
||||
headers.insert("Expires", format!("{epoch_expires:010}").parse().unwrap());
|
||||
@@ -55,7 +54,7 @@ pub fn pre_sign_v2(
|
||||
let string_to_sign = pre_string_to_sign_v2(&req, virtual_host);
|
||||
let signature = hex(hmac_sha1(secret_access_key, string_to_sign));
|
||||
|
||||
let result = serde_urlencoded::from_str::<HashMap<String, String>>(req.uri_ref().unwrap().query().unwrap());
|
||||
let result = serde_urlencoded::from_str::<HashMap<String, String>>(req.uri().query().unwrap());
|
||||
let mut query = result.unwrap_or_default();
|
||||
if get_host_addr(&req).contains(".storage.googleapis.com") {
|
||||
query.insert("GoogleAccessId".to_string(), access_key_id.to_string());
|
||||
@@ -65,15 +64,17 @@ pub fn pre_sign_v2(
|
||||
|
||||
query.insert("Expires".to_string(), format!("{epoch_expires:010}"));
|
||||
|
||||
let uri = req.uri_ref().unwrap().clone();
|
||||
let mut parts = req.uri_ref().unwrap().clone().into_parts();
|
||||
let uri = req.uri().clone();
|
||||
let mut parts = req.uri().clone().into_parts();
|
||||
parts.path_and_query = Some(
|
||||
format!("{}?{}&Signature={}", uri.path(), serde_urlencoded::to_string(&query).unwrap(), signature)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
req.uri(Uri::from_parts(parts).unwrap())
|
||||
*req.uri_mut() = Uri::from_parts(parts).unwrap();
|
||||
|
||||
req
|
||||
}
|
||||
|
||||
fn _post_pre_sign_signature_v2(policy_base64: &str, secret_access_key: &str) -> String {
|
||||
@@ -81,12 +82,12 @@ fn _post_pre_sign_signature_v2(policy_base64: &str, secret_access_key: &str) ->
|
||||
}
|
||||
|
||||
pub fn sign_v2(
|
||||
mut req: request::Builder,
|
||||
mut req: request::Request<Body>,
|
||||
_content_len: i64,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
virtual_host: bool,
|
||||
) -> request::Builder {
|
||||
) -> request::Request<Body> {
|
||||
if access_key_id.is_empty() || secret_access_key.is_empty() {
|
||||
return req;
|
||||
}
|
||||
@@ -95,7 +96,7 @@ pub fn sign_v2(
|
||||
let d2 = d.replace_time(time::Time::from_hms(0, 0, 0).unwrap());
|
||||
|
||||
let string_to_sign = string_to_sign_v2(&req, virtual_host);
|
||||
let headers = req.headers_mut().expect("err");
|
||||
let headers = req.headers_mut();
|
||||
|
||||
let date = headers.get("Date").unwrap();
|
||||
if date.to_str().unwrap() == "" {
|
||||
@@ -117,7 +118,7 @@ pub fn sign_v2(
|
||||
req
|
||||
}
|
||||
|
||||
fn pre_string_to_sign_v2(req: &request::Builder, virtual_host: bool) -> String {
|
||||
fn pre_string_to_sign_v2(req: &request::Request<Body>, virtual_host: bool) -> String {
|
||||
let mut buf = BytesMut::new();
|
||||
write_pre_sign_v2_headers(&mut buf, req);
|
||||
write_canonicalized_headers(&mut buf, req);
|
||||
@@ -125,18 +126,18 @@ fn pre_string_to_sign_v2(req: &request::Builder, virtual_host: bool) -> String {
|
||||
String::from_utf8(buf.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
fn write_pre_sign_v2_headers(buf: &mut BytesMut, req: &request::Builder) {
|
||||
let _ = buf.write_str(req.method_ref().unwrap().as_str());
|
||||
fn write_pre_sign_v2_headers(buf: &mut BytesMut, req: &request::Request<Body>) {
|
||||
let _ = buf.write_str(req.method().as_str());
|
||||
let _ = buf.write_char('\n');
|
||||
let _ = buf.write_str(req.headers_ref().unwrap().get("Content-Md5").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_str(req.headers().get("Content-Md5").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_char('\n');
|
||||
let _ = buf.write_str(req.headers_ref().unwrap().get("Content-Type").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_str(req.headers().get("Content-Type").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_char('\n');
|
||||
let _ = buf.write_str(req.headers_ref().unwrap().get("Expires").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_str(req.headers().get("Expires").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_char('\n');
|
||||
}
|
||||
|
||||
fn string_to_sign_v2(req: &request::Builder, virtual_host: bool) -> String {
|
||||
fn string_to_sign_v2(req: &request::Request<Body>, virtual_host: bool) -> String {
|
||||
let mut buf = BytesMut::new();
|
||||
write_sign_v2_headers(&mut buf, req);
|
||||
write_canonicalized_headers(&mut buf, req);
|
||||
@@ -144,27 +145,27 @@ fn string_to_sign_v2(req: &request::Builder, virtual_host: bool) -> String {
|
||||
String::from_utf8(buf.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
fn write_sign_v2_headers(buf: &mut BytesMut, req: &request::Builder) {
|
||||
let _ = buf.write_str(req.method_ref().unwrap().as_str());
|
||||
fn write_sign_v2_headers(buf: &mut BytesMut, req: &request::Request<Body>) {
|
||||
let headers = req.headers();
|
||||
let _ = buf.write_str(req.method().as_str());
|
||||
let _ = buf.write_char('\n');
|
||||
let _ = buf.write_str(req.headers_ref().unwrap().get("Content-Md5").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_str(headers.get("Content-Md5").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_char('\n');
|
||||
let _ = buf.write_str(req.headers_ref().unwrap().get("Content-Type").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_str(headers.get("Content-Type").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_char('\n');
|
||||
let _ = buf.write_str(req.headers_ref().unwrap().get("Date").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_str(headers.get("Date").unwrap().to_str().unwrap());
|
||||
let _ = buf.write_char('\n');
|
||||
}
|
||||
|
||||
fn write_canonicalized_headers(buf: &mut BytesMut, req: &request::Builder) {
|
||||
fn write_canonicalized_headers(buf: &mut BytesMut, req: &request::Request<Body>) {
|
||||
let mut proto_headers = Vec::<String>::new();
|
||||
let mut vals = HashMap::<String, Vec<String>>::new();
|
||||
for k in req.headers_ref().expect("err").keys() {
|
||||
for k in req.headers().keys() {
|
||||
let lk = k.as_str().to_lowercase();
|
||||
if lk.starts_with("x-amz") {
|
||||
proto_headers.push(lk.clone());
|
||||
let vv = req
|
||||
.headers_ref()
|
||||
.expect("err")
|
||||
.headers()
|
||||
.get_all(k)
|
||||
.iter()
|
||||
.map(|e| e.to_str().unwrap().to_string())
|
||||
@@ -210,12 +211,12 @@ const INCLUDED_QUERY: &[&str] = &[
|
||||
"website",
|
||||
];
|
||||
|
||||
fn write_canonicalized_resource(buf: &mut BytesMut, req: &request::Builder, virtual_host: bool) {
|
||||
let request_url = req.uri_ref().unwrap();
|
||||
fn write_canonicalized_resource(buf: &mut BytesMut, req: &request::Request<Body>, virtual_host: bool) {
|
||||
let request_url = req.uri();
|
||||
let _ = buf.write_str(&encode_url2path(req, virtual_host));
|
||||
if request_url.query().unwrap() != "" {
|
||||
let mut n: i64 = 0;
|
||||
let result = serde_urlencoded::from_str::<HashMap<String, Vec<String>>>(req.uri_ref().unwrap().query().unwrap());
|
||||
let result = serde_urlencoded::from_str::<HashMap<String, Vec<String>>>(req.uri().query().unwrap());
|
||||
let vals = result.unwrap_or_default();
|
||||
for resource in INCLUDED_QUERY {
|
||||
let vv = &vals[*resource];
|
||||
|
||||
@@ -26,6 +26,7 @@ use super::constants::UNSIGNED_PAYLOAD;
|
||||
use super::request_signature_streaming_unsigned_trailer::streaming_unsigned_v4;
|
||||
use super::utils::{get_host_addr, sign_v4_trim_all};
|
||||
use rustfs_utils::crypto::{hex, hex_sha256, hmac_sha256};
|
||||
use s3s::Body;
|
||||
|
||||
pub const SIGN_V4_ALGORITHM: &str = "AWS4-HMAC-SHA256";
|
||||
pub const SERVICE_TYPE_S3: &str = "s3";
|
||||
@@ -76,8 +77,8 @@ fn get_credential(access_key_id: &str, location: &str, t: OffsetDateTime, servic
|
||||
s
|
||||
}
|
||||
|
||||
fn get_hashed_payload(req: &request::Builder) -> String {
|
||||
let headers = req.headers_ref().unwrap();
|
||||
fn get_hashed_payload(req: &request::Request<Body>) -> String {
|
||||
let headers = req.headers();
|
||||
let mut hashed_payload = "";
|
||||
if let Some(payload) = headers.get("X-Amz-Content-Sha256") {
|
||||
hashed_payload = payload.to_str().unwrap();
|
||||
@@ -88,17 +89,16 @@ fn get_hashed_payload(req: &request::Builder) -> String {
|
||||
hashed_payload.to_string()
|
||||
}
|
||||
|
||||
fn get_canonical_headers(req: &request::Builder, ignored_headers: &HashMap<String, bool>) -> String {
|
||||
fn get_canonical_headers(req: &request::Request<Body>, ignored_headers: &HashMap<String, bool>) -> String {
|
||||
let mut headers = Vec::<String>::new();
|
||||
let mut vals = HashMap::<String, Vec<String>>::new();
|
||||
for k in req.headers_ref().expect("err").keys() {
|
||||
for k in req.headers().keys() {
|
||||
if ignored_headers.get(&k.to_string()).is_some() {
|
||||
continue;
|
||||
}
|
||||
headers.push(k.as_str().to_lowercase());
|
||||
let vv = req
|
||||
.headers_ref()
|
||||
.expect("err")
|
||||
.headers()
|
||||
.get_all(k)
|
||||
.iter()
|
||||
.map(|e| e.to_str().unwrap().to_string())
|
||||
@@ -146,9 +146,9 @@ fn header_exists(key: &str, headers: &[String]) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn get_signed_headers(req: &request::Builder, ignored_headers: &HashMap<String, bool>) -> String {
|
||||
fn get_signed_headers(req: &request::Request<Body>, ignored_headers: &HashMap<String, bool>) -> String {
|
||||
let mut headers = Vec::<String>::new();
|
||||
let headers_ref = req.headers_ref().expect("err");
|
||||
let headers_ref = req.headers();
|
||||
debug!("get_signed_headers headers: {:?}", headers_ref);
|
||||
for (k, _) in headers_ref {
|
||||
if ignored_headers.get(&k.to_string()).is_some() {
|
||||
@@ -163,9 +163,9 @@ fn get_signed_headers(req: &request::Builder, ignored_headers: &HashMap<String,
|
||||
headers.join(";")
|
||||
}
|
||||
|
||||
fn get_canonical_request(req: &request::Builder, ignored_headers: &HashMap<String, bool>, hashed_payload: &str) -> String {
|
||||
fn get_canonical_request(req: &request::Request<Body>, ignored_headers: &HashMap<String, bool>, hashed_payload: &str) -> String {
|
||||
let mut canonical_query_string = "".to_string();
|
||||
if let Some(q) = req.uri_ref().unwrap().query() {
|
||||
if let Some(q) = req.uri().query() {
|
||||
// Parse query string into key-value pairs
|
||||
let mut query_params: Vec<(String, String)> = Vec::new();
|
||||
for param in q.split('&') {
|
||||
@@ -187,8 +187,8 @@ fn get_canonical_request(req: &request::Builder, ignored_headers: &HashMap<Strin
|
||||
}
|
||||
|
||||
let canonical_request = [
|
||||
req.method_ref().unwrap().to_string(),
|
||||
req.uri_ref().unwrap().path().to_string(),
|
||||
req.method().to_string(),
|
||||
req.uri().path().to_string(),
|
||||
canonical_query_string,
|
||||
get_canonical_headers(req, ignored_headers),
|
||||
get_signed_headers(req, ignored_headers),
|
||||
@@ -210,14 +210,14 @@ fn get_string_to_sign_v4(t: OffsetDateTime, location: &str, canonical_request: &
|
||||
}
|
||||
|
||||
pub fn pre_sign_v4(
|
||||
req: request::Builder,
|
||||
req: request::Request<Body>,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
session_token: &str,
|
||||
location: &str,
|
||||
expires: i64,
|
||||
t: OffsetDateTime,
|
||||
) -> request::Builder {
|
||||
) -> request::Request<Body> {
|
||||
if access_key_id.is_empty() || secret_access_key.is_empty() {
|
||||
return req;
|
||||
}
|
||||
@@ -226,7 +226,7 @@ pub fn pre_sign_v4(
|
||||
let signed_headers = get_signed_headers(&req, &v4_ignored_headers);
|
||||
|
||||
let mut query = <Vec<(String, String)>>::new();
|
||||
if let Some(q) = req.uri_ref().unwrap().query() {
|
||||
if let Some(q) = req.uri().query() {
|
||||
let result = serde_urlencoded::from_str::<Vec<(String, String)>>(q);
|
||||
query = result.unwrap_or_default();
|
||||
}
|
||||
@@ -240,14 +240,15 @@ pub fn pre_sign_v4(
|
||||
query.push(("X-Amz-Security-Token".to_string(), session_token.to_string()));
|
||||
}
|
||||
|
||||
let uri = req.uri_ref().unwrap().clone();
|
||||
let mut parts = req.uri_ref().unwrap().clone().into_parts();
|
||||
let uri = req.uri().clone();
|
||||
let mut parts = req.uri().clone().into_parts();
|
||||
parts.path_and_query = Some(
|
||||
format!("{}?{}", uri.path(), serde_urlencoded::to_string(&query).unwrap())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
let req = req.uri(Uri::from_parts(parts).unwrap());
|
||||
let mut req = req;
|
||||
*req.uri_mut() = Uri::from_parts(parts).unwrap();
|
||||
|
||||
let canonical_request = get_canonical_request(&req, &v4_ignored_headers, &get_hashed_payload(&req));
|
||||
let string_to_sign = get_string_to_sign_v4(t, location, &canonical_request, SERVICE_TYPE_S3);
|
||||
@@ -256,8 +257,8 @@ pub fn pre_sign_v4(
|
||||
let signing_key = get_signing_key(secret_access_key, location, t, SERVICE_TYPE_S3);
|
||||
let signature = get_signature(signing_key, &string_to_sign);
|
||||
|
||||
let uri = req.uri_ref().unwrap().clone();
|
||||
let mut parts = req.uri_ref().unwrap().clone().into_parts();
|
||||
let uri = req.uri().clone();
|
||||
let mut parts = req.uri().clone().into_parts();
|
||||
parts.path_and_query = Some(
|
||||
format!(
|
||||
"{}?{}&X-Amz-Signature={}",
|
||||
@@ -269,7 +270,9 @@ pub fn pre_sign_v4(
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
req.uri(Uri::from_parts(parts).unwrap())
|
||||
*req.uri_mut() = Uri::from_parts(parts).unwrap();
|
||||
|
||||
req
|
||||
}
|
||||
|
||||
fn _post_pre_sign_signature_v4(policy_base64: &str, t: OffsetDateTime, secret_access_key: &str, location: &str) -> String {
|
||||
@@ -278,13 +281,18 @@ fn _post_pre_sign_signature_v4(policy_base64: &str, t: OffsetDateTime, secret_ac
|
||||
get_signature(signing_key, policy_base64)
|
||||
}
|
||||
|
||||
fn _sign_v4_sts(req: request::Builder, access_key_id: &str, secret_access_key: &str, location: &str) -> request::Builder {
|
||||
fn _sign_v4_sts(
|
||||
req: request::Request<Body>,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
location: &str,
|
||||
) -> request::Request<Body> {
|
||||
sign_v4_inner(req, 0, access_key_id, secret_access_key, "", location, SERVICE_TYPE_STS, HeaderMap::new())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn sign_v4_inner(
|
||||
mut req: request::Builder,
|
||||
mut req: request::Request<Body>,
|
||||
content_len: i64,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
@@ -292,7 +300,7 @@ fn sign_v4_inner(
|
||||
location: &str,
|
||||
service_type: &str,
|
||||
trailer: HeaderMap,
|
||||
) -> request::Builder {
|
||||
) -> request::Request<Body> {
|
||||
if access_key_id.is_empty() || secret_access_key.is_empty() {
|
||||
return req;
|
||||
}
|
||||
@@ -300,7 +308,7 @@ fn sign_v4_inner(
|
||||
let t = OffsetDateTime::now_utc();
|
||||
let t2 = t.replace_time(time::Time::from_hms(0, 0, 0).unwrap());
|
||||
|
||||
let headers = req.headers_mut().expect("err");
|
||||
let headers = req.headers_mut();
|
||||
let format = format_description!("[year][month][day]T[hour][minute][second]Z");
|
||||
headers.insert("X-Amz-Date", t.format(&format).unwrap().to_string().parse().unwrap());
|
||||
|
||||
@@ -330,7 +338,7 @@ fn sign_v4_inner(
|
||||
let signature = get_signature(signing_key, &string_to_sign);
|
||||
//debug!("\n\ncanonical_request: \n{}\nstring_to_sign: \n{}\nsignature: \n{}\n\n", &canonical_request, &string_to_sign, &signature);
|
||||
|
||||
let headers = req.headers_mut().expect("err");
|
||||
let headers = req.headers_mut();
|
||||
|
||||
let auth = format!("{SIGN_V4_ALGORITHM} Credential={credential}, SignedHeaders={signed_headers}, Signature={signature}");
|
||||
headers.insert("Authorization", auth.parse().unwrap());
|
||||
@@ -345,14 +353,14 @@ fn sign_v4_inner(
|
||||
req
|
||||
}
|
||||
|
||||
fn _unsigned_trailer(mut req: request::Builder, content_len: i64, trailer: HeaderMap) {
|
||||
fn _unsigned_trailer(mut req: request::Request<Body>, content_len: i64, trailer: HeaderMap) {
|
||||
if !trailer.is_empty() {
|
||||
return;
|
||||
}
|
||||
let t = OffsetDateTime::now_utc();
|
||||
let t = t.replace_time(time::Time::from_hms(0, 0, 0).unwrap());
|
||||
|
||||
let headers = req.headers_mut().expect("err");
|
||||
let headers = req.headers_mut();
|
||||
let format = format_description!("[year][month][day]T[hour][minute][second]Z");
|
||||
headers.insert("X-Amz-Date", t.format(&format).unwrap().to_string().parse().unwrap());
|
||||
|
||||
@@ -372,13 +380,13 @@ fn _unsigned_trailer(mut req: request::Builder, content_len: i64, trailer: Heade
|
||||
}
|
||||
|
||||
pub fn sign_v4(
|
||||
req: request::Builder,
|
||||
req: request::Request<Body>,
|
||||
content_len: i64,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
session_token: &str,
|
||||
location: &str,
|
||||
) -> request::Builder {
|
||||
) -> request::Request<Body> {
|
||||
sign_v4_inner(
|
||||
req,
|
||||
content_len,
|
||||
@@ -392,13 +400,13 @@ pub fn sign_v4(
|
||||
}
|
||||
|
||||
pub fn sign_v4_trailer(
|
||||
req: request::Builder,
|
||||
req: request::Request<Body>,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
session_token: &str,
|
||||
location: &str,
|
||||
trailer: HeaderMap,
|
||||
) -> request::Builder {
|
||||
) -> request::Request<Body> {
|
||||
sign_v4_inner(
|
||||
req,
|
||||
0,
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
|
||||
use http::request;
|
||||
|
||||
pub fn get_host_addr(req: &request::Builder) -> String {
|
||||
let host = req.headers_ref().expect("err").get("host");
|
||||
let uri = req.uri_ref().unwrap();
|
||||
use s3s::Body;
|
||||
|
||||
pub fn get_host_addr(req: &request::Request<Body>) -> String {
|
||||
let host = req.headers().get("host");
|
||||
let uri = req.uri();
|
||||
let req_host;
|
||||
if let Some(port) = uri.port() {
|
||||
req_host = format!("{}:{}", uri.host().unwrap(), port);
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Utils - Utility Library
|
||||
|
||||
<p align="center">
|
||||
<strong>Essential utility functions and common tools for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Utils** is the utility library for the [RustFS](https://rustfs.com) distributed object storage system. It provides a comprehensive collection of utility functions, helper tools, and common functionality used across all RustFS modules, including system operations, cryptographic utilities, compression, and cross-platform compatibility tools.
|
||||
|
||||
> **Note:** This is a foundational submodule of RustFS that provides essential utility functions for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔧 System Utilities
|
||||
|
||||
- **Cross-Platform Operations**: Unified system operations across platforms
|
||||
- **Process Management**: Process spawning and management utilities
|
||||
- **Resource Monitoring**: CPU, memory, and disk usage monitoring
|
||||
- **Network Utilities**: Network interface and connectivity tools
|
||||
|
||||
### 📁 File System Utilities
|
||||
|
||||
- **Path Manipulation**: Advanced path handling and normalization
|
||||
- **File Operations**: Safe file operations with atomic writes
|
||||
- **Directory Management**: Recursive directory operations
|
||||
- **Symbolic Link Handling**: Cross-platform symlink management
|
||||
|
||||
### 🗜️ Compression & Encoding
|
||||
|
||||
- **Multiple Algorithms**: Support for gzip, zstd, lz4, and more
|
||||
- **Streaming Compression**: Memory-efficient streaming compression
|
||||
- **Base64 Encoding**: High-performance base64 operations
|
||||
- **URL Encoding**: Safe URL encoding and decoding
|
||||
|
||||
### 🔐 Cryptographic Utilities
|
||||
|
||||
- **Hash Functions**: MD5, SHA1, SHA256, XXHash implementations
|
||||
- **Random Generation**: Cryptographically secure random utilities
|
||||
- **Certificate Handling**: X.509 certificate parsing and validation
|
||||
- **Key Generation**: Secure key generation utilities
|
||||
|
||||
### 🌐 Network Utilities
|
||||
|
||||
- **HTTP Helpers**: HTTP client and server utilities
|
||||
- **DNS Resolution**: DNS lookup and resolution tools
|
||||
- **Network Interface**: Interface detection and configuration
|
||||
- **Protocol Utilities**: Various network protocol helpers
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-utils = "0.1.0"
|
||||
|
||||
# Or with specific features
|
||||
rustfs-utils = { version = "0.1.0", features = ["compression", "crypto", "network"] }
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-utils = { version = "0.1.0", features = ["full"] }
|
||||
```
|
||||
|
||||
Available features:
|
||||
|
||||
- `compression` - Compression and decompression utilities
|
||||
- `crypto` - Cryptographic functions and utilities
|
||||
- `network` - Network-related utilities
|
||||
- `path` - Advanced path manipulation tools
|
||||
- `system` - System monitoring and management
|
||||
- `full` - All features enabled
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### File System Utilities
|
||||
|
||||
```rust
|
||||
use rustfs_utils::fs::{ensure_dir, atomic_write, safe_remove};
|
||||
use rustfs_utils::path::{normalize_path, is_subdirectory};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Ensure directory exists
|
||||
ensure_dir("/path/to/directory")?;
|
||||
|
||||
// Atomic file write
|
||||
atomic_write("/path/to/file.txt", b"Hello, World!")?;
|
||||
|
||||
// Path normalization
|
||||
let normalized = normalize_path("./some/../path/./file.txt");
|
||||
println!("Normalized: {}", normalized.display());
|
||||
|
||||
// Check if path is subdirectory
|
||||
if is_subdirectory("/safe/path", "/safe/path/subdir") {
|
||||
println!("Path is safe");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Compression Utilities
|
||||
|
||||
```rust
|
||||
use rustfs_utils::compress::{compress_data, decompress_data, Algorithm};
|
||||
|
||||
fn compression_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let data = b"This is some test data to compress";
|
||||
|
||||
// Compress with different algorithms
|
||||
let gzip_compressed = compress_data(data, Algorithm::Gzip)?;
|
||||
let zstd_compressed = compress_data(data, Algorithm::Zstd)?;
|
||||
let lz4_compressed = compress_data(data, Algorithm::Lz4)?;
|
||||
|
||||
// Decompress
|
||||
let decompressed = decompress_data(&gzip_compressed, Algorithm::Gzip)?;
|
||||
assert_eq!(data, decompressed.as_slice());
|
||||
|
||||
println!("Original size: {}", data.len());
|
||||
println!("Gzip compressed: {}", gzip_compressed.len());
|
||||
println!("Zstd compressed: {}", zstd_compressed.len());
|
||||
println!("LZ4 compressed: {}", lz4_compressed.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Cryptographic Utilities
|
||||
|
||||
```rust
|
||||
use rustfs_utils::crypto::{hash_data, random_bytes, generate_key};
|
||||
use rustfs_utils::crypto::HashAlgorithm;
|
||||
|
||||
fn crypto_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let data = b"Important data to hash";
|
||||
|
||||
// Generate hashes
|
||||
let md5_hash = hash_data(data, HashAlgorithm::MD5)?;
|
||||
let sha256_hash = hash_data(data, HashAlgorithm::SHA256)?;
|
||||
let xxhash = hash_data(data, HashAlgorithm::XXHash64)?;
|
||||
|
||||
println!("MD5: {}", hex::encode(md5_hash));
|
||||
println!("SHA256: {}", hex::encode(sha256_hash));
|
||||
println!("XXHash64: {}", hex::encode(xxhash));
|
||||
|
||||
// Generate secure random data
|
||||
let random_data = random_bytes(32)?;
|
||||
println!("Random data: {}", hex::encode(random_data));
|
||||
|
||||
// Generate cryptographic key
|
||||
let key = generate_key(256)?; // 256-bit key
|
||||
println!("Generated key: {}", hex::encode(key));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### System Monitoring
|
||||
|
||||
```rust
|
||||
use rustfs_utils::sys::{get_system_info, monitor_resources, DiskUsage};
|
||||
|
||||
async fn system_monitoring_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Get system information
|
||||
let sys_info = get_system_info().await?;
|
||||
println!("OS: {} {}", sys_info.os_name, sys_info.os_version);
|
||||
println!("CPU: {} cores", sys_info.cpu_cores);
|
||||
println!("Total Memory: {} GB", sys_info.total_memory / 1024 / 1024 / 1024);
|
||||
|
||||
// Monitor disk usage
|
||||
let disk_usage = DiskUsage::for_path("/var/data")?;
|
||||
println!("Disk space: {} / {} bytes", disk_usage.used, disk_usage.total);
|
||||
println!("Available: {} bytes", disk_usage.available);
|
||||
|
||||
// Monitor resources
|
||||
let resources = monitor_resources().await?;
|
||||
println!("CPU Usage: {:.2}%", resources.cpu_percent);
|
||||
println!("Memory Usage: {:.2}%", resources.memory_percent);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Network Utilities
|
||||
|
||||
```rust
|
||||
use rustfs_utils::net::{resolve_hostname, get_local_ip, is_port_available};
|
||||
|
||||
async fn network_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// DNS resolution
|
||||
let addresses = resolve_hostname("example.com").await?;
|
||||
for addr in addresses {
|
||||
println!("Resolved address: {}", addr);
|
||||
}
|
||||
|
||||
// Get local IP
|
||||
let local_ip = get_local_ip().await?;
|
||||
println!("Local IP: {}", local_ip);
|
||||
|
||||
// Check port availability
|
||||
if is_port_available(8080).await? {
|
||||
println!("Port 8080 is available");
|
||||
} else {
|
||||
println!("Port 8080 is in use");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Certificate Utilities
|
||||
|
||||
```rust
|
||||
use rustfs_utils::certs::{parse_certificate, validate_certificate_chain, CertificateInfo};
|
||||
|
||||
fn certificate_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cert_pem = include_str!("../test_data/certificate.pem");
|
||||
|
||||
// Parse certificate
|
||||
let cert_info = parse_certificate(cert_pem)?;
|
||||
println!("Subject: {}", cert_info.subject);
|
||||
println!("Issuer: {}", cert_info.issuer);
|
||||
println!("Valid from: {}", cert_info.not_before);
|
||||
println!("Valid until: {}", cert_info.not_after);
|
||||
|
||||
// Validate certificate chain
|
||||
let ca_certs = vec![/* CA certificates */];
|
||||
let is_valid = validate_certificate_chain(&cert_info, &ca_certs)?;
|
||||
|
||||
if is_valid {
|
||||
println!("Certificate chain is valid");
|
||||
} else {
|
||||
println!("Certificate chain is invalid");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Encoding Utilities
|
||||
|
||||
```rust
|
||||
use rustfs_utils::encoding::{base64_encode, base64_decode, url_encode, url_decode};
|
||||
|
||||
fn encoding_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let data = b"Hello, World!";
|
||||
|
||||
// Base64 encoding
|
||||
let encoded = base64_encode(data);
|
||||
let decoded = base64_decode(&encoded)?;
|
||||
assert_eq!(data, decoded.as_slice());
|
||||
|
||||
// URL encoding
|
||||
let url = "https://example.com/path with spaces?param=value&other=data";
|
||||
let encoded_url = url_encode(url);
|
||||
let decoded_url = url_decode(&encoded_url)?;
|
||||
assert_eq!(url, decoded_url);
|
||||
|
||||
println!("Base64 encoded: {}", encoded);
|
||||
println!("URL encoded: {}", encoded_url);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Utils Module Structure
|
||||
|
||||
```
|
||||
Utils Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Public API Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ File System │ Compression │ Crypto │ Network │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ System Info │ Encoding │ Certs │ Path Utils │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Platform Abstraction Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Operating System Integration │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Feature Overview
|
||||
|
||||
| Category | Features | Platform Support |
|
||||
|----------|----------|------------------|
|
||||
| File System | Atomic operations, path manipulation | All platforms |
|
||||
| Compression | Gzip, Zstd, LZ4, Brotli | All platforms |
|
||||
| Cryptography | Hashing, random generation, keys | All platforms |
|
||||
| System | Resource monitoring, process management | Linux, macOS, Windows |
|
||||
| Network | DNS, connectivity, interface detection | All platforms |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run tests for specific features
|
||||
cargo test --features compression
|
||||
cargo test --features crypto
|
||||
cargo test --features network
|
||||
|
||||
# Run tests with all features
|
||||
cargo test --features full
|
||||
|
||||
# Run benchmarks
|
||||
cargo bench
|
||||
```
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
The utils library is optimized for performance:
|
||||
|
||||
- **Zero-Copy Operations**: Minimize memory allocations where possible
|
||||
- **Lazy Evaluation**: Defer expensive operations until needed
|
||||
- **Platform Optimization**: Use platform-specific optimizations
|
||||
- **Efficient Algorithms**: Choose the most efficient algorithms for each task
|
||||
|
||||
### Benchmarks
|
||||
|
||||
| Operation | Performance | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| Path Normalization | ~50 ns | Uses efficient string operations |
|
||||
| Base64 Encoding | ~1.2 GB/s | SIMD-optimized implementation |
|
||||
| XXHash64 | ~15 GB/s | Hardware-accelerated when available |
|
||||
| File Copy | ~2 GB/s | Platform-optimized copy operations |
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Architecture**: x86_64, aarch64, and others
|
||||
- **Dependencies**: Minimal external dependencies
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS ECStore](../ecstore) - Erasure coding storage engine
|
||||
- [RustFS Crypto](../crypto) - Cryptographic operations
|
||||
- [RustFS Config](../config) - Configuration management
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Utils API Reference](https://docs.rustfs.com/utils/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🔧 by the RustFS Team
|
||||
</p>
|
||||
@@ -1,198 +0,0 @@
|
||||
// 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.
|
||||
|
||||
use md5::{Digest as Md5Digest, Md5};
|
||||
use sha2::{
|
||||
Sha256 as sha_sha256,
|
||||
digest::{Reset, Update},
|
||||
};
|
||||
pub trait Hasher {
|
||||
fn write(&mut self, bytes: &[u8]);
|
||||
fn reset(&mut self);
|
||||
fn sum(&mut self) -> String;
|
||||
fn size(&self) -> usize;
|
||||
fn block_size(&self) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum HashType {
|
||||
#[default]
|
||||
Undefined,
|
||||
Uuid(Uuid),
|
||||
Md5(MD5),
|
||||
Sha256(Sha256),
|
||||
}
|
||||
|
||||
impl Hasher for HashType {
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
match self {
|
||||
HashType::Md5(md5) => md5.write(bytes),
|
||||
HashType::Sha256(sha256) => sha256.write(bytes),
|
||||
HashType::Uuid(uuid) => uuid.write(bytes),
|
||||
HashType::Undefined => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
match self {
|
||||
HashType::Md5(md5) => md5.reset(),
|
||||
HashType::Sha256(sha256) => sha256.reset(),
|
||||
HashType::Uuid(uuid) => uuid.reset(),
|
||||
HashType::Undefined => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn sum(&mut self) -> String {
|
||||
match self {
|
||||
HashType::Md5(md5) => md5.sum(),
|
||||
HashType::Sha256(sha256) => sha256.sum(),
|
||||
HashType::Uuid(uuid) => uuid.sum(),
|
||||
HashType::Undefined => "".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
match self {
|
||||
HashType::Md5(md5) => md5.size(),
|
||||
HashType::Sha256(sha256) => sha256.size(),
|
||||
HashType::Uuid(uuid) => uuid.size(),
|
||||
HashType::Undefined => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn block_size(&self) -> usize {
|
||||
match self {
|
||||
HashType::Md5(md5) => md5.block_size(),
|
||||
HashType::Sha256(sha256) => sha256.block_size(),
|
||||
HashType::Uuid(uuid) => uuid.block_size(),
|
||||
HashType::Undefined => 64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Sha256 {
|
||||
hasher: sha_sha256,
|
||||
}
|
||||
|
||||
impl Sha256 {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hasher: sha_sha256::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for Sha256 {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hasher for Sha256 {
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
Update::update(&mut self.hasher, bytes);
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
Reset::reset(&mut self.hasher);
|
||||
}
|
||||
|
||||
fn sum(&mut self) -> String {
|
||||
hex_simd::encode_to_string(self.hasher.clone().finalize(), hex_simd::AsciiCase::Lower)
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
32
|
||||
}
|
||||
|
||||
fn block_size(&self) -> usize {
|
||||
64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MD5 {
|
||||
hasher: Md5,
|
||||
}
|
||||
|
||||
impl MD5 {
|
||||
pub fn new() -> Self {
|
||||
Self { hasher: Md5::new() }
|
||||
}
|
||||
}
|
||||
impl Default for MD5 {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hasher for MD5 {
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
Md5Digest::update(&mut self.hasher, bytes);
|
||||
}
|
||||
|
||||
fn reset(&mut self) {}
|
||||
|
||||
fn sum(&mut self) -> String {
|
||||
hex_simd::encode_to_string(self.hasher.clone().finalize(), hex_simd::AsciiCase::Lower)
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
32
|
||||
}
|
||||
|
||||
fn block_size(&self) -> usize {
|
||||
64
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Uuid {
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl Uuid {
|
||||
pub fn new(id: String) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
}
|
||||
|
||||
impl Hasher for Uuid {
|
||||
fn write(&mut self, _bytes: &[u8]) {}
|
||||
|
||||
fn reset(&mut self) {}
|
||||
|
||||
fn sum(&mut self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
self.id.len()
|
||||
}
|
||||
|
||||
fn block_size(&self) -> usize {
|
||||
64
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sum_sha256_hex(data: &[u8]) -> String {
|
||||
let mut hash = Sha256::new();
|
||||
hash.write(data);
|
||||
base64_simd::URL_SAFE_NO_PAD.encode_to_string(hash.sum())
|
||||
}
|
||||
|
||||
pub fn sum_md5_base64(data: &[u8]) -> String {
|
||||
let mut hash = MD5::new();
|
||||
hash.write(data);
|
||||
base64_simd::URL_SAFE_NO_PAD.encode_to_string(hash.sum())
|
||||
}
|
||||
@@ -30,8 +30,6 @@ pub mod io;
|
||||
#[cfg(feature = "hash")]
|
||||
pub mod hash;
|
||||
|
||||
pub mod hasher;
|
||||
|
||||
#[cfg(feature = "os")]
|
||||
pub mod os;
|
||||
|
||||
|
||||
@@ -18,10 +18,11 @@ use futures::{Stream, StreamExt};
|
||||
use hyper::client::conn::http2::Builder;
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
use lazy_static::lazy_static;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::Display,
|
||||
net::{IpAddr, Ipv6Addr, SocketAddr, TcpListener, ToSocketAddrs},
|
||||
net::{IpAddr, SocketAddr, TcpListener, ToSocketAddrs},
|
||||
};
|
||||
use transform_stream::AsyncTryStream;
|
||||
use url::{Host, Url};
|
||||
@@ -201,7 +202,7 @@ pub fn parse_and_resolve_address(addr_str: &str) -> std::io::Result<SocketAddr>
|
||||
} else {
|
||||
port
|
||||
};
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), final_port)
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), final_port)
|
||||
} else {
|
||||
let mut addr = check_local_server_addr(addr_str)?; // assume check_local_server_addr is available here
|
||||
if addr.port() == 0 {
|
||||
@@ -477,12 +478,12 @@ mod test {
|
||||
fn test_parse_and_resolve_address() {
|
||||
// Test port-only format
|
||||
let result = parse_and_resolve_address(":8080").unwrap();
|
||||
assert_eq!(result.ip(), IpAddr::V6(Ipv6Addr::UNSPECIFIED));
|
||||
assert_eq!(result.ip(), IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)));
|
||||
assert_eq!(result.port(), 8080);
|
||||
|
||||
// Test port-only format with port 0 (should get available port)
|
||||
let result = parse_and_resolve_address(":0").unwrap();
|
||||
assert_eq!(result.ip(), IpAddr::V6(Ipv6Addr::UNSPECIFIED));
|
||||
assert_eq!(result.ip(), IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)));
|
||||
assert!(result.port() > 0);
|
||||
|
||||
// Test localhost with port
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Workers - Background Job Processing
|
||||
|
||||
<p align="center">
|
||||
<strong>Distributed background job processing system for RustFS object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Workers** provides a distributed background job processing system for the [RustFS](https://rustfs.com) distributed object storage system. It handles asynchronous tasks such as data replication, cleanup, healing, indexing, and other maintenance operations across the cluster.
|
||||
|
||||
> **Note:** This is a core submodule of RustFS that provides essential background processing capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔄 Job Processing
|
||||
|
||||
- **Distributed Execution**: Jobs run across multiple cluster nodes
|
||||
- **Priority Queues**: Multiple priority levels for job scheduling
|
||||
- **Retry Logic**: Automatic retry with exponential backoff
|
||||
- **Dead Letter Queue**: Failed job isolation and analysis
|
||||
|
||||
### 🛠️ Built-in Workers
|
||||
|
||||
- **Replication Worker**: Data replication across nodes
|
||||
- **Cleanup Worker**: Garbage collection and cleanup
|
||||
- **Healing Worker**: Data integrity repair
|
||||
- **Indexing Worker**: Metadata indexing and search
|
||||
- **Metrics Worker**: Performance metrics collection
|
||||
|
||||
### 🚀 Scalability Features
|
||||
|
||||
- **Horizontal Scaling**: Add worker nodes dynamically
|
||||
- **Load Balancing**: Intelligent job distribution
|
||||
- **Circuit Breaker**: Prevent cascading failures
|
||||
- **Rate Limiting**: Control resource consumption
|
||||
|
||||
### 🔧 Management & Monitoring
|
||||
|
||||
- **Job Tracking**: Real-time job status monitoring
|
||||
- **Health Checks**: Worker health and availability
|
||||
- **Metrics Collection**: Performance and throughput metrics
|
||||
- **Administrative Interface**: Job management and control
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-workers = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Worker Setup
|
||||
|
||||
```rust
|
||||
use rustfs_workers::{WorkerManager, WorkerConfig, JobQueue};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create worker configuration
|
||||
let config = WorkerConfig {
|
||||
worker_id: "worker-1".to_string(),
|
||||
max_concurrent_jobs: 10,
|
||||
job_timeout: Duration::from_secs(300),
|
||||
retry_limit: 3,
|
||||
cleanup_interval: Duration::from_secs(60),
|
||||
};
|
||||
|
||||
// Create worker manager
|
||||
let worker_manager = WorkerManager::new(config).await?;
|
||||
|
||||
// Start worker processing
|
||||
worker_manager.start().await?;
|
||||
|
||||
// Keep running
|
||||
tokio::signal::ctrl_c().await?;
|
||||
worker_manager.shutdown().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Job Definition and Scheduling
|
||||
|
||||
```rust
|
||||
use rustfs_workers::{Job, JobBuilder, JobPriority, JobQueue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplicationJob {
|
||||
pub source_path: String,
|
||||
pub target_nodes: Vec<String>,
|
||||
pub replication_factor: u32,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Job for ReplicationJob {
|
||||
async fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Starting replication job for: {}", self.source_path);
|
||||
|
||||
// Perform replication logic
|
||||
for node in &self.target_nodes {
|
||||
self.replicate_to_node(node).await?;
|
||||
}
|
||||
|
||||
println!("Replication job completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn job_type(&self) -> &str {
|
||||
"replication"
|
||||
}
|
||||
|
||||
fn max_retries(&self) -> u32 {
|
||||
3
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplicationJob {
|
||||
async fn replicate_to_node(&self, node: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Implementation for replicating data to a specific node
|
||||
println!("Replicating {} to node: {}", self.source_path, node);
|
||||
tokio::time::sleep(Duration::from_secs(1)).await; // Simulate work
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn schedule_replication_job() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let job_queue = JobQueue::new().await?;
|
||||
|
||||
// Create replication job
|
||||
let job = ReplicationJob {
|
||||
source_path: "/bucket/important-file.txt".to_string(),
|
||||
target_nodes: vec!["node-2".to_string(), "node-3".to_string()],
|
||||
replication_factor: 2,
|
||||
};
|
||||
|
||||
// Schedule job with high priority
|
||||
let job_id = job_queue.schedule_job(
|
||||
Box::new(job),
|
||||
JobPriority::High,
|
||||
None, // Execute immediately
|
||||
).await?;
|
||||
|
||||
println!("Scheduled replication job with ID: {}", job_id);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Worker Implementation
|
||||
|
||||
```rust
|
||||
use rustfs_workers::{Worker, WorkerContext, JobResult};
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub struct CleanupWorker {
|
||||
storage_path: String,
|
||||
max_file_age: Duration,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Worker for CleanupWorker {
|
||||
async fn process_job(&self, job: Box<dyn Job>, context: &WorkerContext) -> JobResult {
|
||||
match job.job_type() {
|
||||
"cleanup" => {
|
||||
if let Some(cleanup_job) = job.as_any().downcast_ref::<CleanupJob>() {
|
||||
self.execute_cleanup(cleanup_job, context).await
|
||||
} else {
|
||||
JobResult::Failed("Invalid job type for cleanup worker".to_string())
|
||||
}
|
||||
}
|
||||
_ => JobResult::Skipped,
|
||||
}
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> bool {
|
||||
// Check if storage is accessible
|
||||
tokio::fs::metadata(&self.storage_path).await.is_ok()
|
||||
}
|
||||
|
||||
fn worker_type(&self) -> &str {
|
||||
"cleanup"
|
||||
}
|
||||
}
|
||||
|
||||
impl CleanupWorker {
|
||||
pub fn new(storage_path: String, max_file_age: Duration) -> Self {
|
||||
Self {
|
||||
storage_path,
|
||||
max_file_age,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_cleanup(&self, job: &CleanupJob, context: &WorkerContext) -> JobResult {
|
||||
println!("Starting cleanup job for: {}", job.target_path);
|
||||
|
||||
match self.cleanup_old_files(&job.target_path).await {
|
||||
Ok(cleaned_count) => {
|
||||
context.update_metrics("files_cleaned", cleaned_count).await;
|
||||
JobResult::Success
|
||||
}
|
||||
Err(e) => JobResult::Failed(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup_old_files(&self, path: &str) -> Result<u64, Box<dyn std::error::Error>> {
|
||||
let mut cleaned_count = 0;
|
||||
// Implementation for cleaning up old files
|
||||
// ... cleanup logic ...
|
||||
Ok(cleaned_count)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Job Queue Management
|
||||
|
||||
```rust
|
||||
use rustfs_workers::{JobQueue, JobFilter, JobStatus};
|
||||
|
||||
async fn job_queue_management() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let job_queue = JobQueue::new().await?;
|
||||
|
||||
// List pending jobs
|
||||
let pending_jobs = job_queue.list_jobs(JobFilter {
|
||||
status: Some(JobStatus::Pending),
|
||||
job_type: None,
|
||||
priority: None,
|
||||
limit: Some(100),
|
||||
}).await?;
|
||||
|
||||
println!("Pending jobs: {}", pending_jobs.len());
|
||||
|
||||
// Cancel a job
|
||||
let job_id = "job-123";
|
||||
job_queue.cancel_job(job_id).await?;
|
||||
|
||||
// Retry failed jobs
|
||||
let failed_jobs = job_queue.list_jobs(JobFilter {
|
||||
status: Some(JobStatus::Failed),
|
||||
job_type: None,
|
||||
priority: None,
|
||||
limit: Some(50),
|
||||
}).await?;
|
||||
|
||||
for job in failed_jobs {
|
||||
job_queue.retry_job(&job.id).await?;
|
||||
}
|
||||
|
||||
// Get job statistics
|
||||
let stats = job_queue.get_statistics().await?;
|
||||
println!("Job statistics: {:?}", stats);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Distributed Worker Coordination
|
||||
|
||||
```rust
|
||||
use rustfs_workers::{WorkerCluster, WorkerNode, ClusterConfig};
|
||||
|
||||
async fn distributed_worker_setup() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cluster_config = ClusterConfig {
|
||||
node_id: "worker-node-1".to_string(),
|
||||
cluster_endpoint: "https://cluster.rustfs.local".to_string(),
|
||||
heartbeat_interval: Duration::from_secs(30),
|
||||
leader_election_timeout: Duration::from_secs(60),
|
||||
};
|
||||
|
||||
// Create worker cluster
|
||||
let cluster = WorkerCluster::new(cluster_config).await?;
|
||||
|
||||
// Register worker types
|
||||
cluster.register_worker_type("replication", Box::new(ReplicationWorkerFactory)).await?;
|
||||
cluster.register_worker_type("cleanup", Box::new(CleanupWorkerFactory)).await?;
|
||||
cluster.register_worker_type("healing", Box::new(HealingWorkerFactory)).await?;
|
||||
|
||||
// Start cluster participation
|
||||
cluster.join().await?;
|
||||
|
||||
// Handle cluster events
|
||||
let mut event_receiver = cluster.event_receiver();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = event_receiver.recv().await {
|
||||
match event {
|
||||
ClusterEvent::NodeJoined(node) => {
|
||||
println!("Worker node joined: {}", node.id);
|
||||
}
|
||||
ClusterEvent::NodeLeft(node) => {
|
||||
println!("Worker node left: {}", node.id);
|
||||
}
|
||||
ClusterEvent::LeadershipChanged(new_leader) => {
|
||||
println!("New cluster leader: {}", new_leader);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Job Monitoring and Metrics
|
||||
|
||||
```rust
|
||||
use rustfs_workers::{JobMonitor, WorkerMetrics, AlertConfig};
|
||||
|
||||
async fn job_monitoring_setup() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let monitor = JobMonitor::new().await?;
|
||||
|
||||
// Set up alerting
|
||||
let alert_config = AlertConfig {
|
||||
failed_job_threshold: 10,
|
||||
worker_down_threshold: Duration::from_secs(300),
|
||||
queue_size_threshold: 1000,
|
||||
notification_endpoint: "https://alerts.example.com/webhook".to_string(),
|
||||
};
|
||||
|
||||
monitor.configure_alerts(alert_config).await?;
|
||||
|
||||
// Start monitoring
|
||||
monitor.start_monitoring().await?;
|
||||
|
||||
// Get real-time metrics
|
||||
let metrics = monitor.get_metrics().await?;
|
||||
println!("Worker metrics: {:?}", metrics);
|
||||
|
||||
// Set up periodic reporting
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if let Ok(metrics) = monitor.get_metrics().await {
|
||||
println!("=== Worker Metrics ===");
|
||||
println!("Active jobs: {}", metrics.active_jobs);
|
||||
println!("Completed jobs: {}", metrics.completed_jobs);
|
||||
println!("Failed jobs: {}", metrics.failed_jobs);
|
||||
println!("Queue size: {}", metrics.queue_size);
|
||||
println!("Worker count: {}", metrics.worker_count);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Workers Architecture
|
||||
|
||||
```
|
||||
Workers Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Job Management API │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Scheduling │ Monitoring │ Queue Mgmt │ Metrics │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Worker Coordination │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Job Queue │ Load Balancer │ Health Check │ Retry │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Distributed Execution │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Worker Types
|
||||
|
||||
| Worker Type | Purpose | Characteristics |
|
||||
|-------------|---------|----------------|
|
||||
| Replication | Data replication | I/O intensive, network bound |
|
||||
| Cleanup | Garbage collection | CPU intensive, periodic |
|
||||
| Healing | Data repair | I/O intensive, high priority |
|
||||
| Indexing | Metadata indexing | CPU intensive, background |
|
||||
| Metrics | Performance monitoring | Low resource, continuous |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test job processing
|
||||
cargo test job_processing
|
||||
|
||||
# Test worker coordination
|
||||
cargo test worker_coordination
|
||||
|
||||
# Test distributed scenarios
|
||||
cargo test distributed
|
||||
|
||||
# Integration tests
|
||||
cargo test --test integration
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Network**: Cluster connectivity required
|
||||
- **Storage**: Persistent queue storage recommended
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Common](../common) - Common types and utilities
|
||||
- [RustFS Lock](../lock) - Distributed locking
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Workers API Reference](https://docs.rustfs.com/workers/)
|
||||
- [Job Processing Guide](https://docs.rustfs.com/jobs/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 🔄 by the RustFS Team
|
||||
</p>
|
||||
@@ -0,0 +1,407 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS Zip - Compression & Archiving
|
||||
|
||||
<p align="center">
|
||||
<strong>High-performance compression and archiving for RustFS object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/en/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS Zip** provides high-performance compression and archiving capabilities for the [RustFS](https://rustfs.com) distributed object storage system. It supports multiple compression algorithms, streaming compression, and efficient archiving operations optimized for storage systems.
|
||||
|
||||
> **Note:** This is a performance-critical submodule of RustFS that provides essential compression capabilities for the distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 📦 Compression Algorithms
|
||||
|
||||
- **Zstandard (Zstd)**: Fast compression with excellent ratios
|
||||
- **LZ4**: Ultra-fast compression for real-time applications
|
||||
- **Gzip**: Industry-standard compression for compatibility
|
||||
- **Brotli**: Web-optimized compression for text content
|
||||
|
||||
### 🚀 Performance Features
|
||||
|
||||
- **Streaming Compression**: Compress data on-the-fly without buffering
|
||||
- **Parallel Processing**: Multi-threaded compression for large files
|
||||
- **Adaptive Compression**: Automatic algorithm selection based on data
|
||||
- **Hardware Acceleration**: Leverage CPU-specific optimizations
|
||||
|
||||
### 🔧 Archive Management
|
||||
|
||||
- **ZIP Format**: Standard ZIP archive creation and extraction
|
||||
- **TAR Format**: UNIX-style tar archive support
|
||||
- **Custom Formats**: RustFS-optimized archive formats
|
||||
- **Metadata Preservation**: Maintain file attributes and timestamps
|
||||
|
||||
### 📊 Compression Analytics
|
||||
|
||||
- **Ratio Analysis**: Detailed compression statistics
|
||||
- **Performance Metrics**: Compression speed and efficiency
|
||||
- **Content-Type Detection**: Automatic compression algorithm selection
|
||||
- **Deduplication**: Identify and handle duplicate content
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rustfs-zip = "0.1.0"
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
|
||||
### Basic Compression
|
||||
|
||||
```rust
|
||||
use rustfs_zip::{Compressor, CompressionLevel, CompressionAlgorithm};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create compressor
|
||||
let compressor = Compressor::new(CompressionAlgorithm::Zstd, CompressionLevel::Default);
|
||||
|
||||
// Compress data
|
||||
let input_data = b"Hello, World! This is some test data to compress.";
|
||||
let compressed = compressor.compress(input_data).await?;
|
||||
|
||||
println!("Original size: {} bytes", input_data.len());
|
||||
println!("Compressed size: {} bytes", compressed.len());
|
||||
println!("Compression ratio: {:.2}%",
|
||||
(1.0 - compressed.len() as f64 / input_data.len() as f64) * 100.0);
|
||||
|
||||
// Decompress data
|
||||
let decompressed = compressor.decompress(&compressed).await?;
|
||||
assert_eq!(input_data, decompressed.as_slice());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Compression
|
||||
|
||||
```rust
|
||||
use rustfs_zip::{StreamingCompressor, StreamingDecompressor};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
async fn streaming_compression_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create streaming compressor
|
||||
let mut compressor = StreamingCompressor::new(
|
||||
CompressionAlgorithm::Zstd,
|
||||
CompressionLevel::Fast,
|
||||
)?;
|
||||
|
||||
// Compress streaming data
|
||||
let input = tokio::fs::File::open("large_file.txt").await?;
|
||||
let output = tokio::fs::File::create("compressed_file.zst").await?;
|
||||
|
||||
let mut reader = tokio::io::BufReader::new(input);
|
||||
let mut writer = tokio::io::BufWriter::new(output);
|
||||
|
||||
// Stream compression
|
||||
let mut buffer = vec![0u8; 8192];
|
||||
loop {
|
||||
let bytes_read = reader.read(&mut buffer).await?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let compressed_chunk = compressor.compress_chunk(&buffer[..bytes_read]).await?;
|
||||
writer.write_all(&compressed_chunk).await?;
|
||||
}
|
||||
|
||||
// Finalize compression
|
||||
let final_chunk = compressor.finalize().await?;
|
||||
writer.write_all(&final_chunk).await?;
|
||||
writer.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Archive Creation
|
||||
|
||||
```rust
|
||||
use rustfs_zip::{ZipArchive, ArchiveBuilder, CompressionMethod};
|
||||
|
||||
async fn create_archive_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create archive builder
|
||||
let mut builder = ArchiveBuilder::new("backup.zip".to_string());
|
||||
|
||||
// Add files to archive
|
||||
builder.add_file("config.json", "config/app.json", CompressionMethod::Deflate).await?;
|
||||
builder.add_file("data.txt", "data/sample.txt", CompressionMethod::Store).await?;
|
||||
|
||||
// Add directory
|
||||
builder.add_directory("logs/", "application_logs/").await?;
|
||||
|
||||
// Create archive
|
||||
let archive = builder.build().await?;
|
||||
|
||||
println!("Archive created: {}", archive.path());
|
||||
println!("Total files: {}", archive.file_count());
|
||||
println!("Compressed size: {} bytes", archive.compressed_size());
|
||||
println!("Uncompressed size: {} bytes", archive.uncompressed_size());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Archive Extraction
|
||||
|
||||
```rust
|
||||
use rustfs_zip::{ZipExtractor, ExtractionOptions};
|
||||
|
||||
async fn extract_archive_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create extractor
|
||||
let extractor = ZipExtractor::new("backup.zip".to_string());
|
||||
|
||||
// List archive contents
|
||||
let entries = extractor.list_entries().await?;
|
||||
for entry in &entries {
|
||||
println!("File: {} ({} bytes)", entry.name, entry.size);
|
||||
}
|
||||
|
||||
// Extract specific file
|
||||
let file_data = extractor.extract_file("config.json").await?;
|
||||
println!("Extracted config.json: {} bytes", file_data.len());
|
||||
|
||||
// Extract all files
|
||||
let extraction_options = ExtractionOptions {
|
||||
output_directory: "extracted/".to_string(),
|
||||
preserve_paths: true,
|
||||
overwrite_existing: false,
|
||||
};
|
||||
|
||||
extractor.extract_all(extraction_options).await?;
|
||||
println!("Archive extracted successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Adaptive Compression
|
||||
|
||||
```rust
|
||||
use rustfs_zip::{AdaptiveCompressor, ContentAnalyzer, CompressionProfile};
|
||||
|
||||
async fn adaptive_compression_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create adaptive compressor
|
||||
let mut compressor = AdaptiveCompressor::new();
|
||||
|
||||
// Configure compression profiles
|
||||
compressor.add_profile(CompressionProfile {
|
||||
content_type: "text/*".to_string(),
|
||||
algorithm: CompressionAlgorithm::Brotli,
|
||||
level: CompressionLevel::High,
|
||||
min_size: 1024,
|
||||
});
|
||||
|
||||
compressor.add_profile(CompressionProfile {
|
||||
content_type: "image/*".to_string(),
|
||||
algorithm: CompressionAlgorithm::Lz4,
|
||||
level: CompressionLevel::Fast,
|
||||
min_size: 10240,
|
||||
});
|
||||
|
||||
// Compress different types of content
|
||||
let text_content = std::fs::read("document.txt")?;
|
||||
let image_content = std::fs::read("photo.jpg")?;
|
||||
|
||||
// Analyze and compress
|
||||
let text_result = compressor.compress_adaptive(&text_content, Some("text/plain")).await?;
|
||||
let image_result = compressor.compress_adaptive(&image_content, Some("image/jpeg")).await?;
|
||||
|
||||
println!("Text compression: {} -> {} bytes ({})",
|
||||
text_content.len(), text_result.compressed_size, text_result.algorithm);
|
||||
println!("Image compression: {} -> {} bytes ({})",
|
||||
image_content.len(), image_result.compressed_size, image_result.algorithm);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Compression
|
||||
|
||||
```rust
|
||||
use rustfs_zip::{ParallelCompressor, CompressionJob};
|
||||
|
||||
async fn parallel_compression_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create parallel compressor
|
||||
let compressor = ParallelCompressor::new(4); // 4 worker threads
|
||||
|
||||
// Prepare compression jobs
|
||||
let jobs = vec![
|
||||
CompressionJob {
|
||||
id: "file1".to_string(),
|
||||
data: std::fs::read("file1.txt")?,
|
||||
algorithm: CompressionAlgorithm::Zstd,
|
||||
level: CompressionLevel::Default,
|
||||
},
|
||||
CompressionJob {
|
||||
id: "file2".to_string(),
|
||||
data: std::fs::read("file2.txt")?,
|
||||
algorithm: CompressionAlgorithm::Lz4,
|
||||
level: CompressionLevel::Fast,
|
||||
},
|
||||
CompressionJob {
|
||||
id: "file3".to_string(),
|
||||
data: std::fs::read("file3.txt")?,
|
||||
algorithm: CompressionAlgorithm::Gzip,
|
||||
level: CompressionLevel::High,
|
||||
},
|
||||
];
|
||||
|
||||
// Execute parallel compression
|
||||
let results = compressor.compress_batch(jobs).await?;
|
||||
|
||||
for result in results {
|
||||
println!("Job {}: {} -> {} bytes",
|
||||
result.job_id, result.original_size, result.compressed_size);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Content Deduplication
|
||||
|
||||
```rust
|
||||
use rustfs_zip::{DeduplicationCompressor, ContentHash};
|
||||
|
||||
async fn deduplication_example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create deduplication compressor
|
||||
let mut compressor = DeduplicationCompressor::new();
|
||||
|
||||
// Add files for compression
|
||||
let file1 = std::fs::read("document1.txt")?;
|
||||
let file2 = std::fs::read("document2.txt")?;
|
||||
let file3 = std::fs::read("document1.txt")?; // Duplicate of file1
|
||||
|
||||
// Compress with deduplication
|
||||
let result1 = compressor.compress_with_dedup("doc1", &file1).await?;
|
||||
let result2 = compressor.compress_with_dedup("doc2", &file2).await?;
|
||||
let result3 = compressor.compress_with_dedup("doc3", &file3).await?;
|
||||
|
||||
println!("File 1: {} bytes -> {} bytes", file1.len(), result1.compressed_size);
|
||||
println!("File 2: {} bytes -> {} bytes", file2.len(), result2.compressed_size);
|
||||
println!("File 3: {} bytes -> {} bytes (deduplicated: {})",
|
||||
file3.len(), result3.compressed_size, result3.is_deduplicated);
|
||||
|
||||
// Get deduplication statistics
|
||||
let stats = compressor.get_dedup_stats();
|
||||
println!("Deduplication saved: {} bytes", stats.bytes_saved);
|
||||
println!("Duplicate files found: {}", stats.duplicate_count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Zip Module Architecture
|
||||
|
||||
```
|
||||
Zip Architecture:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Compression API │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Algorithm │ Streaming │ Archive │ Adaptive │
|
||||
│ Selection │ Compression │ Management │ Compression│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Compression Engines │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Zstd │ LZ4 │ Gzip │ Brotli │ Custom │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Low-Level Compression │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Compression Algorithms
|
||||
|
||||
| Algorithm | Speed | Ratio | Use Case |
|
||||
|-----------|-------|-------|----------|
|
||||
| LZ4 | Very Fast | Good | Real-time compression |
|
||||
| Zstd | Fast | Excellent | General purpose |
|
||||
| Gzip | Medium | Good | Web compatibility |
|
||||
| Brotli | Slow | Excellent | Text/web content |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Test compression algorithms
|
||||
cargo test algorithms
|
||||
|
||||
# Test streaming compression
|
||||
cargo test streaming
|
||||
|
||||
# Test archive operations
|
||||
cargo test archive
|
||||
|
||||
# Benchmark compression performance
|
||||
cargo bench
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Rust**: 1.70.0 or later
|
||||
- **Platforms**: Linux, macOS, Windows
|
||||
- **Dependencies**: Native compression libraries
|
||||
- **Memory**: Sufficient RAM for compression buffers
|
||||
|
||||
## 🌍 Related Projects
|
||||
|
||||
This module is part of the RustFS ecosystem:
|
||||
|
||||
- [RustFS Main](https://github.com/rustfs/rustfs) - Core distributed storage system
|
||||
- [RustFS Rio](../rio) - High-performance I/O
|
||||
- [RustFS Utils](../utils) - Utility functions
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, visit:
|
||||
|
||||
- [RustFS Documentation](https://docs.rustfs.com)
|
||||
- [Zip API Reference](https://docs.rustfs.com/zip/)
|
||||
- [Compression Guide](https://docs.rustfs.com/compression/)
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Documentation](https://docs.rustfs.com) - Complete RustFS manual
|
||||
- [Changelog](https://github.com/rustfs/rustfs/releases) - Release notes and updates
|
||||
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Community support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](https://github.com/rustfs/rustfs/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/rustfs/rustfs/blob/main/LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>RustFS</strong> is a trademark of RustFS, Inc.<br>
|
||||
All other trademarks are the property of their respective owners.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Made with 📦 by the RustFS Team
|
||||
</p>
|
||||
@@ -17,6 +17,8 @@ version: '3.8'
|
||||
services:
|
||||
# RustFS main service
|
||||
rustfs:
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
image: rustfs/rustfs:latest
|
||||
container_name: rustfs-server
|
||||
build:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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.
|
||||
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
components = ["rustfmt", "clippy", "rust-src", "rust-analyzer"]
|
||||
+14
-12
@@ -30,13 +30,21 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
rustfs-zip = { workspace = true }
|
||||
tokio-tar = { workspace = true }
|
||||
rustfs-madmin = { workspace = true }
|
||||
rustfs-s3select-api = { workspace = true }
|
||||
rustfs-appauth = { workspace = true }
|
||||
rustfs-ecstore = { workspace = true }
|
||||
rustfs-policy = { workspace = true }
|
||||
rustfs-common = { workspace = true }
|
||||
rustfs-iam = { workspace = true }
|
||||
rustfs-filemeta.workspace = true
|
||||
rustfs-rio.workspace = true
|
||||
rustfs-config = { workspace = true, features = ["constants", "notify"] }
|
||||
rustfs-notify = { workspace = true }
|
||||
rustfs-obs = { workspace = true }
|
||||
rustfs-utils = { workspace = true, features = ["full"] }
|
||||
rustfs-protos.workspace = true
|
||||
rustfs-s3select-query = { workspace = true }
|
||||
atoi = { workspace = true }
|
||||
atomic_enum = { workspace = true }
|
||||
axum.workspace = true
|
||||
@@ -53,19 +61,13 @@ hyper.workspace = true
|
||||
hyper-util.workspace = true
|
||||
http.workspace = true
|
||||
http-body.workspace = true
|
||||
rustfs-iam = { workspace = true }
|
||||
lazy_static.workspace = true
|
||||
matchit = { workspace = true }
|
||||
mime_guess = { workspace = true }
|
||||
opentelemetry = { workspace = true }
|
||||
percent-encoding = { workspace = true }
|
||||
pin-project-lite.workspace = true
|
||||
rustfs-protos.workspace = true
|
||||
rustfs-s3select-query = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
rustfs-config = { workspace = true, features = ["constants", "notify"] }
|
||||
rustfs-notify = { workspace = true }
|
||||
rustfs-obs = { workspace = true }
|
||||
rustfs-utils = { workspace = true, features = ["full"] }
|
||||
rustls.workspace = true
|
||||
rust-embed = { workspace = true, features = ["interpolate-folder-path"] }
|
||||
s3s.workspace = true
|
||||
@@ -84,9 +86,9 @@ tokio = { workspace = true, features = [
|
||||
"net",
|
||||
"signal",
|
||||
] }
|
||||
tokio-rustls.workspace = true
|
||||
lazy_static.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
tokio-rustls = { workspace = true, features = ["default"] }
|
||||
tokio-tar = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tower.workspace = true
|
||||
tower-http = { workspace = true, features = [
|
||||
@@ -94,13 +96,13 @@ tower-http = { workspace = true, features = [
|
||||
"compression-deflate",
|
||||
"compression-gzip",
|
||||
"cors",
|
||||
"catch-panic",
|
||||
] }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
rustfs-filemeta.workspace = true
|
||||
rustfs-rio.workspace = true
|
||||
zip = { workspace = true }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libsystemd.workspace = true
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ use s3s::stream::{ByteStream, DynByteStream};
|
||||
use s3s::{Body, S3Error, S3Request, S3Response, S3Result, s3_error};
|
||||
use s3s::{S3ErrorCode, StdError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
// use serde_json::to_vec;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
@@ -970,7 +971,7 @@ impl Operation for ListRemoteTargetHandler {
|
||||
|
||||
if let Some(sys) = GLOBAL_Bucket_Target_Sys.get() {
|
||||
let targets = sys.list_targets(Some(bucket), None).await;
|
||||
error!("target sys len {}", targets.len());
|
||||
info!("target sys len {}", targets.len());
|
||||
if targets.is_empty() {
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -1006,43 +1007,65 @@ pub struct RemoveRemoteTargetHandler {}
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for RemoveRemoteTargetHandler {
|
||||
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
error!("remove remote target called");
|
||||
debug!("remove remote target called");
|
||||
let querys = extract_query_params(&_req.uri);
|
||||
let Some(bucket) = querys.get("bucket") else {
|
||||
return Ok(S3Response::new((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Body::from("Bucket parameter is required".to_string()),
|
||||
)));
|
||||
};
|
||||
|
||||
let mut need_delete = true;
|
||||
|
||||
if let Some(arnstr) = querys.get("arn") {
|
||||
if let Some(bucket) = querys.get("bucket") {
|
||||
if bucket.is_empty() {
|
||||
error!("bucket parameter is empty");
|
||||
return Ok(S3Response::new((StatusCode::NOT_FOUND, Body::from("bucket not found".to_string()))));
|
||||
}
|
||||
let _arn = bucket_targets::ARN::parse(arnstr);
|
||||
let _arn = bucket_targets::ARN::parse(arnstr);
|
||||
|
||||
match get_replication_config(bucket).await {
|
||||
Ok((conf, _ts)) => {
|
||||
for ru in conf.rules {
|
||||
let encoded = percent_encode(ru.destination.bucket.as_bytes(), &COLON);
|
||||
let encoded_str = encoded.to_string();
|
||||
if *arnstr == encoded_str {
|
||||
error!("target in use");
|
||||
return Ok(S3Response::new((StatusCode::FORBIDDEN, Body::from("Ok".to_string()))));
|
||||
}
|
||||
info!("bucket: {} and arn str is {} ", encoded_str, arnstr);
|
||||
match get_replication_config(bucket).await {
|
||||
Ok((conf, _ts)) => {
|
||||
for ru in conf.rules {
|
||||
let encoded = percent_encode(ru.destination.bucket.as_bytes(), &COLON);
|
||||
let encoded_str = encoded.to_string();
|
||||
if *arnstr == encoded_str {
|
||||
//error!("target in use");
|
||||
//return Ok(S3Response::new((StatusCode::OK, Body::from("Ok".to_string()))));
|
||||
need_delete = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("get replication config err: {}", err);
|
||||
return Ok(S3Response::new((StatusCode::NOT_FOUND, Body::from(err.to_string()))));
|
||||
//info!("bucket: {} and arn str is {} ", encoded_str, arnstr);
|
||||
}
|
||||
}
|
||||
//percent_decode_str(&arnstr);
|
||||
Err(err) => {
|
||||
error!("get replication config err: {}", err);
|
||||
return Ok(S3Response::new((StatusCode::NOT_FOUND, Body::from(err.to_string()))));
|
||||
}
|
||||
}
|
||||
if need_delete {
|
||||
info!("arn {} is in use, cannot delete", arnstr);
|
||||
let decoded_str = decode(arnstr).unwrap();
|
||||
error!("need delete target is {}", decoded_str);
|
||||
bucket_targets::remove_bucket_target(bucket, arnstr).await;
|
||||
}
|
||||
}
|
||||
//return Err(s3_error!(InvalidArgument, "Invalid bucket name"));
|
||||
//Ok(S3Response::with_headers((StatusCode::OK, Body::from()), header))
|
||||
return Ok(S3Response::new((StatusCode::OK, Body::from("Ok".to_string()))));
|
||||
// List bucket targets and return as JSON to client
|
||||
// match bucket_targets::list_bucket_targets(bucket).await {
|
||||
// Ok(targets) => {
|
||||
// let json_targets = serde_json::to_string(&targets).map_err(|e| {
|
||||
// error!("Serialization error: {}", e);
|
||||
// S3Error::with_message(S3ErrorCode::InternalError, "Failed to serialize targets".to_string())
|
||||
// })?;
|
||||
// return Ok(S3Response::new((StatusCode::OK, Body::from(json_targets))));
|
||||
// }
|
||||
// Err(e) => {
|
||||
// error!("list bucket targets failed: {:?}", e);
|
||||
// return Err(S3Error::with_message(
|
||||
// S3ErrorCode::InternalError,
|
||||
// "list bucket targets failed".to_string(),
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
|
||||
return Ok(S3Response::new((StatusCode::NO_CONTENT, Body::from("".to_string()))));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(unused_variables, unused_mut, unused_must_use)]
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,6 +11,7 @@
|
||||
// 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.
|
||||
#![allow(unused_variables, unused_mut, unused_must_use)]
|
||||
|
||||
use http::{HeaderMap, StatusCode};
|
||||
//use iam::get_global_action_cred;
|
||||
@@ -461,3 +461,182 @@ impl Operation for ClearTier {
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header))
|
||||
}
|
||||
}
|
||||
|
||||
/*pub struct PostRestoreObject {}
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for PostRestoreObject {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let query = {
|
||||
if let Some(query) = req.uri.query() {
|
||||
let input: PostRestoreObject =
|
||||
from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get query failed"))?;
|
||||
input
|
||||
} else {
|
||||
PostRestoreObject::default()
|
||||
}
|
||||
};
|
||||
|
||||
let bucket = params.bucket;
|
||||
if let Err(e) = un_escape_path(params.object) {
|
||||
warn!("post restore object failed, e: {:?}", e);
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("PostRestoreObjectFailed".into()), "post restore object failed"));
|
||||
}
|
||||
|
||||
let Some(store) = new_object_layer_fn() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
|
||||
let get_object_info = store.get_object_info();
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
if req.content_length <= 0 {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrEmptyRequestBody".into()), "post restore object failed"));
|
||||
}
|
||||
let Some(opts) = post_restore_opts(req, bucket, object) else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrEmptyRequestBody".into()), "post restore object failed"));
|
||||
};
|
||||
|
||||
let Some(obj_info) = getObjectInfo(ctx, bucket, object, opts) else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrEmptyRequestBody".into()), "post restore object failed"));
|
||||
};
|
||||
|
||||
if obj_info.transitioned_object.status != lifecycle::TRANSITION_COMPLETE {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrEmptyRequestBody".into()), "post restore object failed"));
|
||||
}
|
||||
|
||||
let mut api_err;
|
||||
let Some(rreq) = parsere_store_request(req.body(), req.content_length) else {
|
||||
let api_err = errorCodes.ToAPIErr(ErrMalformedXML);
|
||||
api_err.description = err.Error()
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrEmptyRequestBody".into()), "post restore object failed"));
|
||||
};
|
||||
let mut status_code = http::StatusCode::OK;
|
||||
let mut already_restored = false;
|
||||
if Err(err) = rreq.validate(store) {
|
||||
api_err = errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
api_err.description = err.Error()
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrEmptyRequestBody".into()), "post restore object failed"));
|
||||
} else {
|
||||
if obj_info.restore_ongoing && rreq.Type != "SELECT" {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrObjectRestoreAlreadyInProgress".into()), "post restore object failed"));
|
||||
}
|
||||
if !obj_info.restore_ongoing && !obj_info.restore_expires.unix_timestamp() == 0 {
|
||||
status_code = http::StatusCode::Accepted;
|
||||
already_restored = true;
|
||||
}
|
||||
}
|
||||
let restore_expiry = lifecycle::expected_expiry_time(OffsetDateTime::now_utc(), rreq.days);
|
||||
let mut metadata = clone_mss(obj_info.user_defined);
|
||||
|
||||
if rreq.type != "SELECT" {
|
||||
obj_info.metadataOnly = true;
|
||||
metadata[xhttp.AmzRestoreExpiryDays] = rreq.days;
|
||||
metadata[xhttp.AmzRestoreRequestDate] = OffsetDateTime::now_utc().format(http::TimeFormat);
|
||||
if already_restored {
|
||||
metadata[xhttp.AmzRestore] = completedRestoreObj(restore_expiry).String()
|
||||
} else {
|
||||
metadata[xhttp.AmzRestore] = ongoingRestoreObj().String()
|
||||
}
|
||||
obj_info.user_defined = metadata;
|
||||
if let Err(err) = store.copy_object(bucket, object, bucket, object, obj_info, ObjectOptions {
|
||||
version_id: obj_info.version_id,
|
||||
}, ObjectOptions {
|
||||
version_id: obj_info.version_id,
|
||||
m_time: obj_info.mod_time,
|
||||
}) {
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ErrInvalidObjectState".into()), "post restore object failed"));
|
||||
}
|
||||
if already_restored {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let restore_object = must_get_uuid();
|
||||
if rreq.output_location.s3.bucket_name != "" {
|
||||
w.Header()[xhttp.AmzRestoreOutputPath] = []string{pathJoin(rreq.OutputLocation.S3.BucketName, rreq.OutputLocation.S3.Prefix, restoreObject)}
|
||||
}
|
||||
w.WriteHeader(status_code)
|
||||
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.SelectParameters.IsEmpty() {
|
||||
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.NewObjectReadSeekCloser(
|
||||
|offset int64| -> (io.ReadCloser, error) {
|
||||
rs := &HTTPRangeSpec{
|
||||
IsSuffixLength: false,
|
||||
Start: offset,
|
||||
End: -1,
|
||||
}
|
||||
return getTransitionedObjectReader(bucket, object, rs, r.Header,
|
||||
obj_info, ObjectOptions {version_id: obj_info.version_id});
|
||||
},
|
||||
actual_size.unwrap(),
|
||||
);
|
||||
if err = rreq.SelectParameters.Open(objectRSC); 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));
|
||||
}
|
||||
let opts = ObjectOptions {
|
||||
transition: TransitionOptions {
|
||||
restore_request: rreq,
|
||||
restore_expiry: restore_expiry,
|
||||
},
|
||||
version_id: objInfo.version_id,
|
||||
}
|
||||
if Err(err) = store.restore_transitioned_object(bucket, object, opts) {
|
||||
format!(format!("unable to restore transitioned bucket/object {}/{}: {}", bucket, object, err.to_string()));
|
||||
return Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header));
|
||||
}
|
||||
|
||||
send_event(EventArgs {
|
||||
EventName: event.ObjectRestoreCompleted,
|
||||
BucketName: bucket,
|
||||
Object: objInfo,
|
||||
ReqParams: extractReqParams(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Host: handlers.GetSourceIP(r),
|
||||
});
|
||||
});
|
||||
|
||||
let mut header = HeaderMap::new();
|
||||
header.insert(CONTENT_TYPE, "application/json".parse().unwrap());
|
||||
|
||||
Ok(S3Response::with_headers((StatusCode::OK, Body::empty()), header))
|
||||
}
|
||||
}*/
|
||||
|
||||
+171
-233
@@ -44,7 +44,6 @@ use hyper_util::{
|
||||
use license::init_license;
|
||||
use rustfs_common::globals::set_global_addr;
|
||||
use rustfs_config::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
|
||||
use rustfs_ecstore::StorageAPI;
|
||||
use rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys;
|
||||
use rustfs_ecstore::cmd::bucket_replication::init_bucket_replication_pool;
|
||||
use rustfs_ecstore::config as ecconfig;
|
||||
@@ -53,18 +52,16 @@ use rustfs_ecstore::heal::background_heal_ops::init_auto_heal;
|
||||
use rustfs_ecstore::rpc::make_server;
|
||||
use rustfs_ecstore::store_api::BucketOptions;
|
||||
use rustfs_ecstore::{
|
||||
endpoints::EndpointServerPools,
|
||||
heal::data_scanner::init_data_scanner,
|
||||
set_global_endpoints,
|
||||
store::{ECStore, init_local_disks},
|
||||
StorageAPI, endpoints::EndpointServerPools, global::set_global_rustfs_port, heal::data_scanner::init_data_scanner,
|
||||
notification_sys::new_global_notification_sys, set_global_endpoints, store::ECStore, store::init_local_disks,
|
||||
update_erasure_type,
|
||||
};
|
||||
use rustfs_ecstore::{global::set_global_rustfs_port, notification_sys::new_global_notification_sys};
|
||||
use rustfs_iam::init_iam_sys;
|
||||
use rustfs_obs::{SystemObserver, init_obs, set_global_guard};
|
||||
use rustfs_protos::proto_gen::node_service::node_service_server::NodeServiceServer;
|
||||
use rustfs_utils::net::parse_and_resolve_address;
|
||||
use rustls::ServerConfig;
|
||||
use s3s::service::S3Service;
|
||||
use s3s::{host::MultiDomain, service::S3ServiceBuilder};
|
||||
use service::hybrid;
|
||||
use socket2::SockRef;
|
||||
@@ -72,11 +69,13 @@ use std::io::{Error, Result};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
#[cfg(unix)]
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tonic::{Request, Status, metadata::MetadataValue};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::catch_panic::CatchPanicLayer;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{Span, debug, error, info, instrument, warn};
|
||||
@@ -128,6 +127,49 @@ async fn main() -> Result<()> {
|
||||
run(opt).await
|
||||
}
|
||||
|
||||
/// Sets up the TLS acceptor if certificates are available.
|
||||
#[instrument(skip(tls_path))]
|
||||
async fn setup_tls_acceptor(tls_path: &str) -> Result<Option<TlsAcceptor>> {
|
||||
if tls_path.is_empty() || tokio::fs::metadata(tls_path).await.is_err() {
|
||||
debug!("TLS path is not provided or does not exist, starting with HTTP");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
debug!("Found TLS directory, checking for certificates");
|
||||
|
||||
// 1. Try to load all certificates from the directory (multi-cert support)
|
||||
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) {
|
||||
if !cert_key_pairs.is_empty() {
|
||||
debug!("Found {} certificates, creating multi-cert resolver", cert_key_pairs.len());
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?));
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
return Ok(Some(TlsAcceptor::from(Arc::new(server_config))));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to legacy single certificate mode
|
||||
let key_path = format!("{tls_path}/{RUSTFS_TLS_KEY}");
|
||||
let cert_path = format!("{tls_path}/{RUSTFS_TLS_CERT}");
|
||||
if tokio::try_join!(tokio::fs::metadata(&key_path), tokio::fs::metadata(&cert_path)).is_ok() {
|
||||
debug!("Found legacy single TLS certificate, starting with HTTPS");
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
let certs = rustfs_utils::load_certs(&cert_path).map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
|
||||
let key = rustfs_utils::load_private_key(&key_path).map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)
|
||||
.map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
return Ok(Some(TlsAcceptor::from(Arc::new(server_config))));
|
||||
}
|
||||
|
||||
debug!("No valid TLS certificates found in the directory, starting with HTTP");
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[instrument(skip(opt))]
|
||||
async fn run(opt: config::Opt) -> Result<()> {
|
||||
debug!("opt: {:?}", &opt);
|
||||
@@ -147,7 +189,6 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
let listener = TcpListener::bind(server_address.clone()).await?;
|
||||
// Obtain the listener address
|
||||
let local_addr: SocketAddr = listener.local_addr()?;
|
||||
// let local_ip = utils::get_local_ip().ok_or(local_addr.ip()).unwrap();
|
||||
let local_ip = rustfs_utils::get_local_ip().ok_or(local_addr.ip()).unwrap();
|
||||
|
||||
// For RPC
|
||||
@@ -203,18 +244,14 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
// This project uses the S3S library to implement S3 services
|
||||
let s3_service = {
|
||||
let store = storage::ecfs::FS::new();
|
||||
// let mut b = S3ServiceBuilder::new(storage::ecfs::FS::new(server_address.clone(), endpoint_pools).await?);
|
||||
let mut b = S3ServiceBuilder::new(store.clone());
|
||||
|
||||
let access_key = opt.access_key.clone();
|
||||
let secret_key = opt.secret_key.clone();
|
||||
// Displays info information
|
||||
debug!("authentication is enabled {}, {}", &access_key, &secret_key);
|
||||
|
||||
b.set_auth(IAMAuth::new(access_key, secret_key));
|
||||
|
||||
b.set_access(store.clone());
|
||||
|
||||
b.set_route(admin::make_admin_route()?);
|
||||
|
||||
if !opt.server_domains.is_empty() {
|
||||
@@ -222,20 +259,6 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
b.set_host(MultiDomain::new(&opt.server_domains).map_err(Error::other)?);
|
||||
}
|
||||
|
||||
// // Enable parsing virtual-hosted-style requests
|
||||
// if let Some(dm) = opt.domain_name {
|
||||
// info!("virtual-hosted-style requests are enabled use domain_name {}", &dm);
|
||||
// b.set_base_domain(dm);
|
||||
// }
|
||||
|
||||
// if domain_name.is_some() {
|
||||
// info!(
|
||||
// "virtual-hosted-style requests are enabled use domain_name {}",
|
||||
// domain_name.as_ref().unwrap()
|
||||
// );
|
||||
// b.set_base_domain(domain_name.unwrap());
|
||||
// }
|
||||
|
||||
b.build()
|
||||
};
|
||||
|
||||
@@ -253,57 +276,8 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
let tls_path = opt.tls_path.clone().unwrap_or_default();
|
||||
let has_tls_certs = tokio::fs::metadata(&tls_path).await.is_ok();
|
||||
let tls_acceptor = if has_tls_certs {
|
||||
debug!("Found TLS directory, checking for certificates");
|
||||
let tls_acceptor = setup_tls_acceptor(opt.tls_path.as_deref().unwrap_or_default()).await?;
|
||||
|
||||
// 1. Try to load all certificates directly (including root and subdirectories)
|
||||
match rustfs_utils::load_all_certs_from_directory(&tls_path) {
|
||||
Ok(cert_key_pairs) if !cert_key_pairs.is_empty() => {
|
||||
debug!("Found {} certificates, starting with HTTPS", cert_key_pairs.len());
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
// create a multi certificate configuration
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?));
|
||||
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
Some(TlsAcceptor::from(Arc::new(server_config)))
|
||||
}
|
||||
_ => {
|
||||
// 2. If the synthesis fails, fall back to the traditional document certificate mode (backward compatible)
|
||||
let key_path = format!("{tls_path}/{RUSTFS_TLS_KEY}");
|
||||
let cert_path = format!("{tls_path}/{RUSTFS_TLS_CERT}");
|
||||
let has_single_cert =
|
||||
tokio::try_join!(tokio::fs::metadata(key_path.clone()), tokio::fs::metadata(cert_path.clone())).is_ok();
|
||||
|
||||
if has_single_cert {
|
||||
debug!("Found legacy single TLS certificate, starting with HTTPS");
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
let certs =
|
||||
rustfs_utils::load_certs(cert_path.as_str()).map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
|
||||
let key = rustfs_utils::load_private_key(key_path.as_str())
|
||||
.map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)
|
||||
.map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
Some(TlsAcceptor::from(Arc::new(server_config)))
|
||||
} else {
|
||||
debug!("No valid TLS certificates found, starting with HTTP");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("TLS certificates not found, starting with HTTP");
|
||||
None
|
||||
};
|
||||
|
||||
let rpc_service = NodeServiceServer::with_interceptor(make_server(), check_auth);
|
||||
let state_manager = ServiceStateManager::new();
|
||||
let worker_state_manager = state_manager.clone();
|
||||
// Update service status to Starting
|
||||
@@ -317,72 +291,11 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
let (mut sigterm_inner, mut sigint_inner) = {
|
||||
// Unix platform specific code
|
||||
let sigterm_inner = match signal(SignalKind::terminate()) {
|
||||
Ok(signal) => signal,
|
||||
Err(e) => {
|
||||
error!("Failed to create SIGTERM signal handler: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let sigint_inner = match signal(SignalKind::interrupt()) {
|
||||
Ok(signal) => signal,
|
||||
Err(e) => {
|
||||
error!("Failed to create SIGINT signal handler: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let sigterm_inner = signal(SignalKind::terminate()).expect("Failed to create SIGTERM signal handler");
|
||||
let sigint_inner = signal(SignalKind::interrupt()).expect("Failed to create SIGINT signal handler");
|
||||
(sigterm_inner, sigint_inner)
|
||||
};
|
||||
|
||||
let hybrid_service = TowerToHyperService::new(
|
||||
tower::ServiceBuilder::new()
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &HttpRequest<_>| {
|
||||
let span = tracing::info_span!("http-request",
|
||||
status_code = tracing::field::Empty,
|
||||
method = %request.method(),
|
||||
uri = %request.uri(),
|
||||
version = ?request.version(),
|
||||
);
|
||||
for (header_name, header_value) in request.headers() {
|
||||
if header_name == "user-agent" || header_name == "content-type" || header_name == "content-length"
|
||||
{
|
||||
span.record(header_name.as_str(), header_value.to_str().unwrap_or("invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
span
|
||||
})
|
||||
.on_request(|request: &HttpRequest<_>, _span: &Span| {
|
||||
info!(
|
||||
counter.rustfs_api_requests_total = 1_u64,
|
||||
key_request_method = %request.method().to_string(),
|
||||
key_request_uri_path = %request.uri().path().to_owned(),
|
||||
"handle request api total",
|
||||
);
|
||||
debug!("http started method: {}, url path: {}", request.method(), request.uri().path())
|
||||
})
|
||||
.on_response(|response: &Response<_>, latency: Duration, _span: &Span| {
|
||||
_span.record("http response status_code", tracing::field::display(response.status()));
|
||||
debug!("http response generated in {:?}", latency)
|
||||
})
|
||||
.on_body_chunk(|chunk: &Bytes, latency: Duration, _span: &Span| {
|
||||
info!(histogram.request.body.len = chunk.len(), "histogram request body length",);
|
||||
debug!("http body sending {} bytes in {:?}", chunk.len(), latency)
|
||||
})
|
||||
.on_eos(|_trailers: Option<&HeaderMap>, stream_duration: Duration, _span: &Span| {
|
||||
debug!("http stream closed after {:?}", stream_duration)
|
||||
})
|
||||
.on_failure(|_error, latency: Duration, _span: &Span| {
|
||||
info!(counter.rustfs_api_requests_failure_total = 1_u64, "handle request api failure total");
|
||||
debug!("http request failure error: {:?} in {:?}", _error, latency)
|
||||
}),
|
||||
)
|
||||
.layer(CorsLayer::permissive())
|
||||
.service(hybrid(s3_service, rpc_service)),
|
||||
);
|
||||
|
||||
let http_server = Arc::new(ConnBuilder::new(TokioExecutor::new()));
|
||||
let mut ctrl_c = std::pin::pin!(tokio::signal::ctrl_c());
|
||||
let graceful = Arc::new(GracefulShutdown::new());
|
||||
@@ -390,42 +303,36 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
|
||||
// service ready
|
||||
worker_state_manager.update(ServiceState::Ready);
|
||||
let value = hybrid_service.clone();
|
||||
let tls_acceptor = tls_acceptor.map(Arc::new);
|
||||
|
||||
loop {
|
||||
debug!("waiting for SIGINT or SIGTERM has_tls_certs: {}", has_tls_certs);
|
||||
// Wait for a connection
|
||||
debug!("Waiting for new connection...");
|
||||
let (socket, _) = {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
tokio::select! {
|
||||
res = listener.accept() => {
|
||||
match res {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("error accepting connection: {err}");
|
||||
continue;
|
||||
}
|
||||
res = listener.accept() => match res {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("error accepting connection: {err}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
_ = ctrl_c.as_mut() => {
|
||||
info!("Ctrl-C received in worker thread");
|
||||
let _ = shutdown_tx_clone.send(());
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
Some(_) = sigint_inner.recv() => {
|
||||
info!("SIGINT received in worker thread");
|
||||
let _ = shutdown_tx_clone.send(());
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
Some(_) = sigterm_inner.recv() => {
|
||||
info!("SIGTERM received in worker thread");
|
||||
let _ = shutdown_tx_clone.send(());
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
_ = shutdown_rx.recv() => {
|
||||
info!("Shutdown signal received in worker thread");
|
||||
break;
|
||||
@@ -435,22 +342,18 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
tokio::select! {
|
||||
res = listener.accept() => {
|
||||
match res {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("error accepting connection: {err}");
|
||||
continue;
|
||||
}
|
||||
res = listener.accept() => match res {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("error accepting connection: {err}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
_ = ctrl_c.as_mut() => {
|
||||
info!("Ctrl-C received in worker thread");
|
||||
let _ = shutdown_tx_clone.send(());
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
_ = shutdown_rx.recv() => {
|
||||
info!("Shutdown signal received in worker thread");
|
||||
break;
|
||||
@@ -470,70 +373,12 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
warn!(?err, "Failed to set set_send_buffer_size");
|
||||
}
|
||||
|
||||
if has_tls_certs {
|
||||
debug!("TLS certificates found, starting with SIGINT");
|
||||
let peer_addr_str = socket.peer_addr().map(|a| a.to_string()).unwrap_or_else(|e| {
|
||||
warn!("Could not get peer address: {}", e);
|
||||
"unknown".to_string()
|
||||
});
|
||||
let tls_socket = match tls_acceptor.as_ref() {
|
||||
Some(acceptor) => match acceptor.accept(socket).await {
|
||||
Ok(tls_socket) => {
|
||||
info!("TLS handshake successful with peer: {}", peer_addr_str);
|
||||
tls_socket
|
||||
}
|
||||
Err(err) => {
|
||||
error!("TLS handshake with peer {} failed: {}", peer_addr_str, err);
|
||||
continue;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
error!(
|
||||
"TLS acceptor is not available, but TLS is enabled. This is a bug. Dropping connection from {}",
|
||||
peer_addr_str
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let http_server_clone = http_server.clone();
|
||||
let value_clone = value.clone();
|
||||
let graceful_clone = graceful.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("Failed to create runtime")
|
||||
.block_on(async move {
|
||||
let conn = http_server_clone.serve_connection(TokioIo::new(tls_socket), value_clone);
|
||||
let conn = graceful_clone.watch(conn);
|
||||
if let Err(err) = conn.await {
|
||||
// Handle hyper::Error and low-level IO errors at a more granular level
|
||||
handle_connection_error(&*err);
|
||||
}
|
||||
});
|
||||
});
|
||||
debug!("TLS handshake success");
|
||||
} else {
|
||||
debug!("Http handshake start");
|
||||
|
||||
let http_server_clone = http_server.clone();
|
||||
let value_clone = value.clone();
|
||||
let graceful_clone = graceful.clone();
|
||||
tokio::spawn(async move {
|
||||
let conn = http_server_clone.serve_connection(TokioIo::new(socket), value_clone);
|
||||
let conn = graceful_clone.watch(conn);
|
||||
if let Err(err) = conn.await {
|
||||
// Handle hyper::Error and low-level IO errors at a more granular level
|
||||
handle_connection_error(&*err);
|
||||
}
|
||||
});
|
||||
debug!("Http handshake success");
|
||||
}
|
||||
process_connection(socket, tls_acceptor.clone(), http_server.clone(), s3_service.clone(), graceful.clone());
|
||||
}
|
||||
|
||||
worker_state_manager.update(ServiceState::Stopping);
|
||||
match Arc::try_unwrap(graceful) {
|
||||
Ok(g) => {
|
||||
// Successfully obtaining unique ownership, you can call shutdown
|
||||
tokio::select! {
|
||||
() = g.shutdown() => {
|
||||
debug!("Gracefully shutdown!");
|
||||
@@ -544,9 +389,7 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
}
|
||||
}
|
||||
Err(arc_graceful) => {
|
||||
// There are other references that cannot be obtained for unique ownership
|
||||
error!("Cannot perform graceful shutdown, other references exist err: {:?}", arc_graceful);
|
||||
// In this case, we can only wait for the timeout
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
debug!("Timeout reached, forcing shutdown");
|
||||
}
|
||||
@@ -589,14 +432,12 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
init_data_scanner().await;
|
||||
// init auto heal
|
||||
init_auto_heal().await;
|
||||
|
||||
// init console configuration
|
||||
init_console_cfg(local_ip, server_port);
|
||||
|
||||
print_server_info();
|
||||
init_bucket_replication_pool().await;
|
||||
|
||||
print_server_info();
|
||||
|
||||
// Async update check (optional)
|
||||
tokio::spawn(async {
|
||||
use crate::update_checker::{UpdateCheckError, check_updates};
|
||||
@@ -664,6 +505,103 @@ async fn run(opt: config::Opt) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process a single incoming TCP connection.
|
||||
///
|
||||
/// This function is executed in a new Tokio task and it will:
|
||||
/// 1. If TLS is configured, perform TLS handshake.
|
||||
/// 2. Build a complete service stack for this connection, including S3, RPC services, and all middleware.
|
||||
/// 3. Use Hyper to handle HTTP requests on this connection.
|
||||
/// 4. Incorporate connections into the management of elegant closures.
|
||||
#[instrument(skip_all, fields(peer_addr = %socket.peer_addr().map(|a| a.to_string()).unwrap_or_else(|_| "unknown".to_string())))]
|
||||
fn process_connection(
|
||||
socket: TcpStream,
|
||||
tls_acceptor: Option<Arc<TlsAcceptor>>,
|
||||
http_server: Arc<ConnBuilder<TokioExecutor>>,
|
||||
s3_service: S3Service,
|
||||
graceful: Arc<GracefulShutdown>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
// Build services inside each connected task to avoid passing complex service types across tasks,
|
||||
// It also ensures that each connection has an independent service instance.
|
||||
let rpc_service = NodeServiceServer::with_interceptor(make_server(), check_auth);
|
||||
let hybrid_service = ServiceBuilder::new()
|
||||
.layer(CatchPanicLayer::new())
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &HttpRequest<_>| {
|
||||
let span = tracing::info_span!("http-request",
|
||||
status_code = tracing::field::Empty,
|
||||
method = %request.method(),
|
||||
uri = %request.uri(),
|
||||
version = ?request.version(),
|
||||
);
|
||||
for (header_name, header_value) in request.headers() {
|
||||
if header_name == "user-agent" || header_name == "content-type" || header_name == "content-length" {
|
||||
span.record(header_name.as_str(), header_value.to_str().unwrap_or("invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
span
|
||||
})
|
||||
.on_request(|request: &HttpRequest<_>, _span: &Span| {
|
||||
info!(
|
||||
counter.rustfs_api_requests_total = 1_u64,
|
||||
key_request_method = %request.method().to_string(),
|
||||
key_request_uri_path = %request.uri().path().to_owned(),
|
||||
"handle request api total",
|
||||
);
|
||||
debug!("http started method: {}, url path: {}", request.method(), request.uri().path())
|
||||
})
|
||||
.on_response(|response: &Response<_>, latency: Duration, _span: &Span| {
|
||||
_span.record("http response status_code", tracing::field::display(response.status()));
|
||||
debug!("http response generated in {:?}", latency)
|
||||
})
|
||||
.on_body_chunk(|chunk: &Bytes, latency: Duration, _span: &Span| {
|
||||
info!(histogram.request.body.len = chunk.len(), "histogram request body length",);
|
||||
debug!("http body sending {} bytes in {:?}", chunk.len(), latency)
|
||||
})
|
||||
.on_eos(|_trailers: Option<&HeaderMap>, stream_duration: Duration, _span: &Span| {
|
||||
debug!("http stream closed after {:?}", stream_duration)
|
||||
})
|
||||
.on_failure(|_error, latency: Duration, _span: &Span| {
|
||||
info!(counter.rustfs_api_requests_failure_total = 1_u64, "handle request api failure total");
|
||||
debug!("http request failure error: {:?} in {:?}", _error, latency)
|
||||
}),
|
||||
)
|
||||
.layer(CorsLayer::permissive())
|
||||
.service(hybrid(s3_service, rpc_service));
|
||||
let hybrid_service = TowerToHyperService::new(hybrid_service);
|
||||
|
||||
// Decide whether to handle HTTPS or HTTP connections based on the existence of TLS Acceptor
|
||||
if let Some(acceptor) = tls_acceptor {
|
||||
debug!("TLS handshake start");
|
||||
match acceptor.accept(socket).await {
|
||||
Ok(tls_socket) => {
|
||||
debug!("TLS handshake successful");
|
||||
let stream = TokioIo::new(tls_socket);
|
||||
let conn = http_server.serve_connection(stream, hybrid_service);
|
||||
if let Err(err) = graceful.watch(conn).await {
|
||||
handle_connection_error(&*err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "TLS handshake failed");
|
||||
return; // Failed to end the task directly
|
||||
}
|
||||
}
|
||||
debug!("TLS handshake success");
|
||||
} else {
|
||||
debug!("Http handshake start");
|
||||
let stream = TokioIo::new(socket);
|
||||
let conn = http_server.serve_connection(stream, hybrid_service);
|
||||
if let Err(err) = graceful.watch(conn).await {
|
||||
handle_connection_error(&*err);
|
||||
}
|
||||
debug!("Http handshake success");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// Handles the shutdown process of the server
|
||||
async fn handle_shutdown(state_manager: &ServiceStateManager, shutdown_tx: &tokio::sync::broadcast::Sender<()>) {
|
||||
info!("Shutdown signal received in main thread");
|
||||
|
||||
@@ -1976,19 +1976,19 @@ impl S3 for FS {
|
||||
..
|
||||
} = req.input;
|
||||
|
||||
let mut lr_retention = false;
|
||||
let rcfg = metadata_sys::get_object_lock_config(&bucket).await;
|
||||
let lr_retention = false;
|
||||
/*let rcfg = metadata_sys::get_object_lock_config(&bucket).await;
|
||||
if let Ok(rcfg) = rcfg {
|
||||
if let Some(rule) = rcfg.0.rule {
|
||||
if let Some(retention) = rule.default_retention {
|
||||
if let Some(mode) = retention.mode {
|
||||
if mode == ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::GOVERNANCE) {
|
||||
//if mode == ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::GOVERNANCE) {
|
||||
lr_retention = true;
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
//info!("lifecycle_configuration: {:?}", &lifecycle_configuration);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user