From f88656172cf65f40ac54d6c7f78aacefb4269211 Mon Sep 17 00:00:00 2001 From: narawat Date: Sat, 29 Aug 2026 09:52:33 +0700 Subject: [PATCH] object lifecycle works --- src/garageS3.jl | 100 ++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 117 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) diff --git a/src/garageS3.jl b/src/garageS3.jl index 9399977..ca264b6 100644 --- a/src/garageS3.jl +++ b/src/garageS3.jl @@ -1,6 +1,7 @@ module garageS3 export + LifecycleExpiration, build_lifecycle_xml, GarageStorage, put_file, get_file, list_files, delete_file, set_lifecycle_expiration, get_lifecycle, delete_lifecycle @@ -266,11 +267,110 @@ function delete_file(storage::GarageStorage, key::String) end +# =================================================================== +# 3. Bucket Lifecycle Configuration +# =================================================================== +""" Lifecycle expiration configuration for a bucket. +# Fields +- `prefix::String`: Object key prefix this rule applies to. +- `days::Int`: Number of days after object creation to expire/delete. +- `enabled::Bool`: Whether the rule is active (default: `true`). +- `id::String`: Unique identifier for the rule (default: auto-generated). +""" +struct LifecycleExpiration + prefix::String + days::Int + enabled::Bool + id::String +end +function LifecycleExpiration(prefix::String, days::Int; enabled::Bool=true, id::String="") + rule_id = isempty(id) ? "expire-" * replace(prefix, "/" => "_") * "-" * string(days) * "d" : id + return LifecycleExpiration(prefix, days, enabled, rule_id) +end +""" Build the XML body for a PutBucketLifecycleConfiguration request. +Supports a single rule with expiration by prefix. +""" +function build_lifecycle_xml(rule::LifecycleExpiration)::String + status = rule.enabled ? "Enabled" : "Disabled" + return """ + + + $(rule.id) + + $(rule.prefix) + + $status + + $(rule.days) + + +""" +end +""" Set bucket lifecycle expiration rule. +# Arguments +- `storage::GarageStorage`: The storage client instance. +- `rule::LifecycleExpiration`: The lifecycle expiration rule. + +# Returns +- `Dict{String, Any}`: Parsed response from the server. + +# Example +```julia +rule = LifecycleExpiration("uploads/", 30) +set_lifecycle_expiration(storage, rule) +``` +""" +function set_lifecycle_expiration(storage::GarageStorage, rule::LifecycleExpiration) + xml_body = build_lifecycle_xml(rule) + args = Dict{String,Any}("body" => xml_body) + return S3.put_bucket_lifecycle_configuration(storage.bucket, args; aws_config=storage.config) +end + +""" Get the bucket lifecycle configuration. + +# Arguments +- `storage::GarageStorage`: The storage client instance. + +# Returns +- `Dict{String, Any}`: Parsed lifecycle configuration, or `nothing` if no lifecycle is configured. + +# Example +```julia +config = get_lifecycle(storage) +``` +""" +function get_lifecycle(storage::GarageStorage)::Union{Dict{String,Any},Nothing} + try + return S3.get_bucket_lifecycle_configuration(storage.bucket; aws_config=storage.config) + catch e + if isa(e, AWS.AWSException) && e.code == "NoSuchLifecycleConfiguration" + return nothing + end + rethrow(e) + end +end + +""" Delete the bucket lifecycle configuration. + +# Arguments +- `storage::GarageStorage`: The storage client instance. + +# Returns +- `Dict{String, Any}`: Parsed response from the server. + +# Example +```julia +delete_lifecycle(storage) +``` +""" +function delete_lifecycle(storage::GarageStorage) + return S3.delete_bucket_lifecycle(storage.bucket; aws_config=storage.config) +end end # module GarageS3 diff --git a/test/runtests.jl b/test/runtests.jl index ea29e64..8ef2cf7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -343,4 +343,121 @@ using UUIDs: uuid4 delete_file(storage, test_key) end + # --- Lifecycle configuration tests --- + + @testset "LifecycleExpiration construction with default ID" begin + rule = LifecycleExpiration("uploads/", 30) + @test rule.prefix == "uploads/" + @test rule.days == 30 + @test rule.enabled == true + @test rule.id == "expire-uploads_-30d" + end + + @testset "LifecycleExpiration construction with custom ID" begin + rule = LifecycleExpiration("data/", 7; id="my-rule") + @test rule.prefix == "data/" + @test rule.days == 7 + @test rule.enabled == true + @test rule.id == "my-rule" + end + + @testset "LifecycleExpiration with disabled flag" begin + rule = LifecycleExpiration("temp/", 1; enabled=false) + @test rule.enabled == false + end + + @testset "LifecycleExpiration with nested prefix" begin + rule = LifecycleExpiration("deep/nested/", 90) + @test rule.prefix == "deep/nested/" + end + + @testset "build_lifecycle_xml generates valid XML" begin + rule = LifecycleExpiration("test/", 14; id="test-rule") + xml = build_lifecycle_xml(rule) + @test occursin("", xml) + @test occursin("test-rule", xml) + @test occursin("test/", xml) + @test occursin("Enabled", xml) + @test occursin("14", xml) + @test occursin("", xml) + end + + @testset "build_lifecycle_xml generates disabled rule XML" begin + rule = LifecycleExpiration("temp/", 1; enabled=false) + xml = build_lifecycle_xml(rule) + @test occursin("Disabled", xml) + end + + @testset "integration: lifecycle CRUD - set and get" begin + storage = GarageStorage( + "https://s3-api.yiem.cc", + "GKb080154a2e5b19100b1b2c6e", + "a2c6b1379c2f731d3e6e5a408dd4d6cffca7511717675f55114ff94828febca1", + "testbucket" + ) + + # Should return nothing when no lifecycle is set + lc = get_lifecycle(storage) + @test lc === nothing + + # Set lifecycle + rule = LifecycleExpiration("lifecycle-test/", 45) + set_lifecycle_expiration(storage, rule) + + # Should return configuration after setting + lc = get_lifecycle(storage) + @test lc !== nothing + @test haskey(lc, "Rule") + + # Delete lifecycle + delete_lifecycle(storage) + + # Should return nothing after deletion + lc = get_lifecycle(storage) + @test lc === nothing + end + + @testset "integration: lifecycle with nested prefix" begin + storage = GarageStorage( + "https://s3-api.yiem.cc", + "GKb080154a2e5b19100b1b2c6e", + "a2c6b1379c2f731d3e6e5a408dd4d6cffca7511717675f55114ff94828febca1", + "testbucket" + ) + + rule = LifecycleExpiration("a/b/c/", 20; id="nested-rule") + set_lifecycle_expiration(storage, rule) + + lc = get_lifecycle(storage) + @test lc !== nothing + @test haskey(lc, "Rule") + + delete_lifecycle(storage) + @test get_lifecycle(storage) === nothing + end + + @testset "integration: lifecycle enabled/disabled toggle" begin + storage = GarageStorage( + "https://s3-api.yiem.cc", + "GKb080154a2e5b19100b1b2c6e", + "a2c6b1379c2f731d3e6e5a408dd4d6cffca7511717675f55114ff94828febca1", + "testbucket" + ) + + # Set enabled rule + rule1 = LifecycleExpiration("toggle/", 10; enabled=true) + set_lifecycle_expiration(storage, rule1) + lc = get_lifecycle(storage) + @test lc !== nothing + + # Override with disabled rule + rule2 = LifecycleExpiration("toggle/", 10; enabled=false) + set_lifecycle_expiration(storage, rule2) + lc = get_lifecycle(storage) + @test lc !== nothing + + delete_lifecycle(storage) + end + end