Files
seaweedFS/weed/pb/filer_pb/filer_pb_helper.go
Konstantin Lebedev 084b377f87 do delete expired entries on s3 list request (#7426)
* do delete expired entries on s3 list request
https://github.com/seaweedfs/seaweedfs/issues/6837

* disable delete expires s3 entry in filer

* pass opt allowDeleteObjectsByTTL to all servers

* delete on get and head

* add lifecycle expiration s3 tests

* fix opt allowDeleteObjectsByTTL for server

* fix test lifecycle expiration

* fix IsExpired

* fix locationPrefix for updateEntriesTTL

* fix s3tests

* resolv  coderabbitai

* GetS3ExpireTime on filer

* go mod

* clear TtlSeconds for volume

* move s3 delete expired entry to filer

* filer delete meta and data

* del unusing func removeExpiredObject

* test s3 put

* test s3 put multipart

* allowDeleteObjectsByTTL by default

* fix pipline tests

* rm dublicate SeaweedFSExpiresS3

* revert expiration tests

* fix updateTTL

* rm log

* resolv comment

* fix delete version object

* fix S3Versioning

* fix delete on FindEntry

* fix delete chunks

* fix sqlite not support concurrent writes/reads

* move deletion out of listing transaction; delete entries and empty folders

* Revert "fix sqlite not support concurrent writes/reads"

This reverts commit 5d5da14e0ed91c613fe5c0ed058f58bb04fba6f0.

* clearer handling on recursive empty directory deletion

* handle listing errors

* strut copying

* reuse code to delete empty folders

* use iterative approach with a queue to avoid recursive WithFilerClient calls

* stop a gRPC stream from the client-side callback is to return a specific error, e.g., io.EOF

* still issue UpdateEntry when the flag must be added

* errors join

* join path

* cleaner

* add context, sort directories by depth (deepest first) to avoid redundant checks

* batched operation, refactoring

* prevent deleting bucket

* constant

* reuse code

* more logging

* refactoring

* s3 TTL time

* Safety check

---------

Co-authored-by: chrislu <chris.lu@gmail.com>
2025-11-05 22:05:54 -08:00

208 lines
5.8 KiB
Go

package filer_pb
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/viant/ptrie"
"google.golang.org/protobuf/proto"
)
const cutoffTimeNewEmptyDir = 3
func (entry *Entry) IsInRemoteOnly() bool {
return len(entry.GetChunks()) == 0 && entry.RemoteEntry != nil && entry.RemoteEntry.RemoteSize > 0
}
func (entry *Entry) IsDirectoryKeyObject() bool {
return entry.IsDirectory && entry.Attributes != nil && entry.Attributes.Mime != ""
}
func (entry *Entry) GetExpiryTime() (expiryTime int64) {
// For S3 objects with lifecycle expiration, use Mtime (modification time)
// For regular TTL entries, use Crtime (creation time) for backward compatibility
if entry.Extended != nil {
if _, hasS3Expiry := entry.Extended[s3_constants.SeaweedFSExpiresS3]; hasS3Expiry {
// S3 lifecycle expiration: base TTL on modification time
expiryTime = entry.Attributes.Mtime
if expiryTime == 0 {
expiryTime = entry.Attributes.Crtime
}
expiryTime += int64(entry.Attributes.TtlSec)
return expiryTime
}
}
// Regular TTL expiration: base on creation time only
expiryTime = entry.Attributes.Crtime + int64(entry.Attributes.TtlSec)
return expiryTime
}
func (entry *Entry) IsExpired() bool {
return entry != nil && entry.Attributes != nil && entry.Attributes.TtlSec > 0 &&
time.Now().Unix() >= entry.GetExpiryTime()
}
func (entry *Entry) FileMode() (fileMode os.FileMode) {
if entry != nil && entry.Attributes != nil {
fileMode = os.FileMode(entry.Attributes.FileMode)
}
return
}
func (entry *Entry) IsOlderDir() bool {
return entry.IsDirectory && entry.Attributes != nil && entry.Attributes.Mime == "" && entry.Attributes.GetCrtime() <= time.Now().Unix()-cutoffTimeNewEmptyDir
}
func ToFileIdObject(fileIdStr string) (*FileId, error) {
t, err := needle.ParseFileIdFromString(fileIdStr)
if err != nil {
return nil, err
}
return &FileId{
VolumeId: uint32(t.VolumeId),
Cookie: uint32(t.Cookie),
FileKey: uint64(t.Key),
}, nil
}
func (fid *FileId) toFileIdString() string {
return needle.NewFileId(needle.VolumeId(fid.VolumeId), fid.FileKey, fid.Cookie).String()
}
func (c *FileChunk) GetFileIdString() string {
if c.FileId != "" {
return c.FileId
}
if c.Fid != nil {
c.FileId = c.Fid.toFileIdString()
return c.FileId
}
return ""
}
func BeforeEntrySerialization(chunks []*FileChunk) {
for _, chunk := range chunks {
if chunk.FileId != "" {
if fid, err := ToFileIdObject(chunk.FileId); err == nil {
chunk.Fid = fid
chunk.FileId = ""
}
}
if chunk.SourceFileId != "" {
if fid, err := ToFileIdObject(chunk.SourceFileId); err == nil {
chunk.SourceFid = fid
chunk.SourceFileId = ""
}
}
}
}
func EnsureFid(chunk *FileChunk) {
if chunk.Fid != nil {
return
}
if fid, err := ToFileIdObject(chunk.FileId); err == nil {
chunk.Fid = fid
}
}
func AfterEntryDeserialization(chunks []*FileChunk) {
for _, chunk := range chunks {
if chunk.Fid != nil && chunk.FileId == "" {
chunk.FileId = chunk.Fid.toFileIdString()
}
if chunk.SourceFid != nil && chunk.SourceFileId == "" {
chunk.SourceFileId = chunk.SourceFid.toFileIdString()
}
}
}
func CreateEntry(ctx context.Context, client SeaweedFilerClient, request *CreateEntryRequest) error {
resp, err := client.CreateEntry(ctx, request)
if err != nil {
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v", request.Directory, request.Entry.Name, request.OExcl, err)
return fmt.Errorf("CreateEntry: %w", err)
}
if resp.Error != "" {
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v", request.Directory, request.Entry.Name, request.OExcl, resp.Error)
return fmt.Errorf("CreateEntry : %v", resp.Error)
}
return nil
}
func UpdateEntry(ctx context.Context, client SeaweedFilerClient, request *UpdateEntryRequest) error {
_, err := client.UpdateEntry(ctx, request)
if err != nil {
glog.V(1).InfofCtx(ctx, "update entry %s/%s :%v", request.Directory, request.Entry.Name, err)
return fmt.Errorf("UpdateEntry: %w", err)
}
return nil
}
func LookupEntry(ctx context.Context, client SeaweedFilerClient, request *LookupDirectoryEntryRequest) (*LookupDirectoryEntryResponse, error) {
resp, err := client.LookupDirectoryEntry(ctx, request)
if err != nil {
if err == ErrNotFound || strings.Contains(err.Error(), ErrNotFound.Error()) {
return nil, ErrNotFound
}
glog.V(3).InfofCtx(ctx, "read %s/%v: %v", request.Directory, request.Name, err)
return nil, fmt.Errorf("LookupEntry1: %w", err)
}
if resp.Entry == nil {
return nil, ErrNotFound
}
return resp, nil
}
var ErrNotFound = errors.New("filer: no entry is found in filer store")
func IsEmpty(event *SubscribeMetadataResponse) bool {
return event.EventNotification.NewEntry == nil && event.EventNotification.OldEntry == nil
}
func IsCreate(event *SubscribeMetadataResponse) bool {
return event.EventNotification.NewEntry != nil && event.EventNotification.OldEntry == nil
}
func IsUpdate(event *SubscribeMetadataResponse) bool {
return event.EventNotification.NewEntry != nil &&
event.EventNotification.OldEntry != nil &&
event.Directory == event.EventNotification.NewParentPath &&
event.EventNotification.NewEntry.Name == event.EventNotification.OldEntry.Name
}
func IsDelete(event *SubscribeMetadataResponse) bool {
return event.EventNotification.NewEntry == nil && event.EventNotification.OldEntry != nil
}
func IsRename(event *SubscribeMetadataResponse) bool {
return event.EventNotification.NewEntry != nil &&
event.EventNotification.OldEntry != nil &&
(event.Directory != event.EventNotification.NewParentPath ||
event.EventNotification.NewEntry.Name != event.EventNotification.OldEntry.Name)
}
var _ = ptrie.KeyProvider(&FilerConf_PathConf{})
func (fp *FilerConf_PathConf) Key() interface{} {
key, _ := proto.Marshal(fp)
return string(key)
}