1diff --git a/go.mod b/go.mod
2index 50c34bf..ff68aa7 100644
3--- a/go.mod
4+++ b/go.mod
5@@ -48,6 +48,7 @@ require (
6 github.com/picosh/pubsub v0.0.0-20241114191831-ec8f16c0eb88
7 github.com/picosh/utils v0.0.0-20250528150332-0721504d3a96
8 github.com/pkg/sftp v1.13.9
9+ github.com/pkg/xattr v0.4.11
10 github.com/prometheus/client_golang v1.21.1
11 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
12 github.com/sendgrid/sendgrid-go v3.16.0+incompatible
13diff --git a/go.sum b/go.sum
14index a63ea40..c22dd98 100644
15--- a/go.sum
16+++ b/go.sum
17@@ -652,6 +652,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
18 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
19 github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
20 github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
21+github.com/pkg/xattr v0.4.11 h1:DA7usy0rTMNMGvm06b5LhZUwiPj708D89S8DkXpMB1E=
22+github.com/pkg/xattr v0.4.11/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
23 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
24 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
25 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26@@ -1086,6 +1088,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
27 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
30+golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
31 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
32 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34diff --git a/pkg/pobj/storage/fs.go b/pkg/pobj/storage/fs.go
35index c703b90..1f4204e 100644
36--- a/pkg/pobj/storage/fs.go
37+++ b/pkg/pobj/storage/fs.go
38@@ -1,6 +1,8 @@
39 package storage
40
41 import (
42+ "crypto/md5"
43+ "encoding/hex"
44 "fmt"
45 "io"
46 "io/fs"
47@@ -14,6 +16,7 @@ import (
48
49 "github.com/picosh/pico/pkg/send/utils"
50 "github.com/picosh/pico/pkg/shared/mime"
51+ "github.com/pkg/xattr"
52 )
53
54 // https://stackoverflow.com/a/32482941
55@@ -113,6 +116,14 @@ func (s *StorageFS) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderA
56 return nil, objInfo, err
57 }
58
59+ attr, err := xattr.FGet(dat, "user.etag")
60+ if err != nil {
61+ s.Logger.Error("xattr read etag", "err", err)
62+ }
63+ etag := string(attr)
64+ s.Logger.Info("xattr get etag", "etag", etag)
65+
66+ objInfo.ETag = etag
67 objInfo.Size = info.Size()
68 objInfo.LastModified = info.ModTime()
69 objInfo.Metadata.Set("content-type", mime.GetMimeType(fpath))
70@@ -130,11 +141,26 @@ func (s *StorageFS) PutObject(bucket Bucket, fpath string, contents io.Reader, e
71 return "", 0, err
72 }
73
74+ // calculate etag
75+ h := md5.New()
76+ tr := io.TeeReader(contents, h)
77+ if _, err := io.Copy(h, tr); err != nil {
78+ return "", 0, err
79+ }
80+ md5Sum := h.Sum(nil)
81+ etag := hex.EncodeToString(md5Sum)
82+
83 size, err := io.Copy(f, contents)
84 if err != nil {
85 return "", 0, err
86 }
87
88+ s.Logger.Info("xattr set etag", "etag", etag)
89+ err = xattr.FSet(f, "user.etag", []byte(etag))
90+ if err != nil {
91+ s.Logger.Error("xattr set etag", "err", err)
92+ }
93+
94 err = f.Close()
95 if err != nil {
96 return "", 0, err