Repository: juicedata/juicefs
Pull request #5847 (2025-03-24 09:57:54)

sdk/python: support warmup/summary/clone/info
_info_ is already implemented as summary

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/juicedata/juicefs/pull/5847?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 50.34%. Comparing base [(`a2715c4`)](https://app.codecov.io/gh/juicedata/juicefs/commit/a2715c4e8a6fbaef99424c996b91a259979a12b5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata) to head [(`6c0321e`)](https://app.codecov.io/gh/juicedata/juicefs/commit/6c0321e1cda87cb2a17729c05bba95073ce7833c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata).
> Report is 39 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             main    #5847       +/-   ##
===========================================
+ Coverage   21.45%   50.34%   +28.89%     
===========================================
  Files          29       29               
  Lines       19342    19954      +612     
===========================================
+ Hits         4149    10046     +5897     
+ Misses      14696     8453     -6243     
- Partials      497     1455      +958     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/juicedata/juicefs/pull/5847?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=juicedata).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/cmd/debug.go b/cmd/debug.go
index 565b1797..89fc70c7 100644
--- a/cmd/debug.go
+++ b/cmd/debug.go
@@ -295,10 +295,6 @@ func reqAndSaveMetric(name string, metric metricItem, outDir string, timeout tim
 	return writer.Flush()
 }
 
-func isUnix() bool {
-	return runtime.GOOS == "linux" || runtime.GOOS == "darwin"
-}
-
 func checkAgent(cmd string) bool {
 	for _, field := range strings.Fields(cmd) {
 		if field == "--no-agent" {
@@ -361,11 +357,6 @@ func collectPprof(ctx *cli.Context, cmd string, pid string, amp string, requireR
 		return nil
 	}
 
-	if !isUnix() {
-		logger.Warnf("Collecting pprof currently only support Linux/macOS")
-		return nil
-	}
-
 	port, err := getPprofPort(pid, amp, requireRootPrivileges)
 	if err != nil {
 		return fmt.Errorf("failed to get pprof port: %v", err)
@@ -414,12 +405,9 @@ func collectPprof(ctx *cli.Context, cmd string, pid string, amp string, requireR
 	return nil
 }
 
-func collectLog(ctx *cli.Context, cmd string, requireRootPrivileges bool, currDir string) error {
-	if !isUnix() {
-		logger.Warnf("Collecting log currently only support Linux/macOS")
-		return nil
-	}
-	if !(strings.Contains(cmd, "-d") || strings.Contains(cmd, "--background")) {
+func collectLog(ctx *cli.Context, cmd string, requireRootPrivileges bool, currDir string, uid string) error {
+	mountdByWinSystem := runtime.GOOS == "windows" && uid == "S-1-5-18" // https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
+	if !(strings.Contains(cmd, "-d") || strings.Contains(cmd, "--background")) && !mountdByWinSystem {
 		logger.Warnf("The juicefs mount by foreground, the log will not be collected")
 		return nil
 	}
@@ -430,15 +418,30 @@ func collectLog(ctx *cli.Context, cmd string, requireRootPrivileges bool, currDi
 	limit := ctx.Uint64("limit")
 	retLogPath := filepath.Join(currDir, "juicefs.log")
 
-	var copyArgs []string
-	if requireRootPrivileges {
-		copyArgs = append(copyArgs, "sudo")
+	if runtime.GOOS == "windows" {
+		// check powershell is installed
+		_, err = exec.LookPath("powershell")
+		if err != nil {
+			logger.Warnf("Powershell is not installed, the log will not be collected")
+			return nil
+		}
+
+		copyArgs := []string{"powershell", "-Command", fmt.Sprintf("Get-Content -Tail %d %s > %s", limit, logPath, retLogPath)}
+		logger.Infof("The last %d lines of %s will be collected", limit, logPath)
+		timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		return exec.CommandContext(timeoutCtx, copyArgs[0], copyArgs[1:]...).Run()
+	} else {
+		var copyArgs []string
+		if requireRootPrivileges {
+			copyArgs = append(copyArgs, "sudo")
+		}
+		copyArgs = append(copyArgs, "/bin/sh", "-c", fmt.Sprintf("tail -n %d %s > %s", limit, logPath, retLogPath))
+		logger.Infof("The last %d lines of %s will be collected", limit, logPath)
+		timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		return exec.CommandContext(timeoutCtx, copyArgs[0], copyArgs[1:]...).Run()
 	}
-	copyArgs = append(copyArgs, "/bin/sh", "-c", fmt.Sprintf("tail -n %d %s > %s", limit, logPath, retLogPath))
-	logger.Infof("The last %d lines of %s will be collected", limit, logPath)
-	timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-	defer cancel()
-	return exec.CommandContext(timeoutCtx, copyArgs[0], copyArgs[1:]...).Run()
 }
 
 func collectSysInfo(ctx *cli.Context, currDir string) error {
@@ -555,7 +558,7 @@ func debug(ctx *cli.Context) error {
 		logger.Errorf("Failed to collect special file: %v", err)
 	}
 
-	if err := collectLog(ctx, cmd, requireRootPrivileges, currDir); err != nil {
+	if err := collectLog(ctx, cmd, requireRootPrivileges, currDir, uid); err != nil {
 		logger.Errorf("Failed to collect log: %v", err)
 	}
 
diff --git a/cmd/debug_windows.go b/cmd/debug_windows.go
index 7f1d7491..a757c9ec 100644
--- a/cmd/debug_windows.go
+++ b/cmd/debug_windows.go
@@ -51,7 +51,7 @@ func getprocessCommandLine(pid int) (string, error) {
 		return sline, nil
 	}
 
-	return "", fmt.Errorf("cannot find command line for pid %d", pid)
+	return "", fmt.Errorf("cannot find command line for pid %d. If the juicefs are mounted at background, Please rerun this with the admin permission.", pid)
 }
 
 func findMountProcess(mp string) (int, error) {
diff --git a/cmd/mount.go b/cmd/mount.go
index 2eb8e23f..9016c523 100644
--- a/cmd/mount.go
+++ b/cmd/mount.go
@@ -534,7 +534,7 @@ func getDefaultLogDir() string {
 			break
 		}
 		fallthrough
-	case "darwin":
+	case "darwin", "windows":
 		homeDir, err := os.UserHomeDir()
 		if err != nil {
 			logger.Fatalf("%v", err)
diff --git a/cmd/mount_unix.go b/cmd/mount_unix.go
index 54ac82aa..e6e3d101 100644
--- a/cmd/mount_unix.go
+++ b/cmd/mount_unix.go
@@ -775,6 +775,7 @@ func launchMount(mp string, conf *vfs.Config) error {
 	}
 	os.Setenv("_FUSE_FD_COMM", serverAddress)
 	serveFuseFD(serverAddress)
+	defer os.Remove(serverAddress)
 
 	path, err := os.Executable()
 	if err != nil {
diff --git a/cmd/mount_windows.go b/cmd/mount_windows.go
index 5cbd5ab7..a59e5517 100644
--- a/cmd/mount_windows.go
+++ b/cmd/mount_windows.go
@@ -17,6 +17,9 @@
 package cmd
 
 import (
+	"os"
+	"path/filepath"
+
 	"github.com/juicedata/juicefs/pkg/meta"
 	"github.com/juicedata/juicefs/pkg/object"
 	"github.com/juicedata/juicefs/pkg/vfs"
@@ -30,6 +33,11 @@ func mountFlags() []cli.Flag {
 			Name:  "o",
 			Usage: "other FUSE options",
 		},
+		&cli.StringFlag{
+			Name:  "log",
+			Value: filepath.Join(getDefaultLogDir(), "juicefs.log"),
+			Usage: "path of log file when running in background",
+		},
 		&cli.StringFlag{
 			Name:  "access-log",
 			Usage: "Access log file",
@@ -60,7 +68,17 @@ func mountFlags() []cli.Flag {
 }
 
 func makeDaemon(c *cli.Context, conf *vfs.Config) error {
-	return winfsp.RunAsSystemSerivce(conf.Format.Name, c.Args().Get(1))
+	logPath := c.String("log")
+	if logPath != "" {
+		if !filepath.IsAbs(logPath) {
+			return cli.Exit("log path must be an absolute path", 1)
+		}
+		if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {
+			return cli.Exit(err, 1)
+		}
+	}
+
+	return winfsp.RunAsSystemSerivce(conf.Format.Name, c.Args().Get(1), logPath)
 }
 
 func makeDaemonForSvc(c *cli.Context, m meta.Meta, metaUrl, listenAddr string) error {
diff --git a/cmd/passfd.go b/cmd/passfd.go
index eed33805..cfdd93c8 100644
--- a/cmd/passfd.go
+++ b/cmd/passfd.go
@@ -162,7 +162,6 @@ func serveFuseFD(path string) {
 		return
 	}
 	go func() {
-		defer os.Remove(path)
 		defer sock.Close()
 		for {
 			conn, err := sock.Accept()
diff --git a/cmd/restore.go b/cmd/restore.go
index 267f0bce..2d40b4ed 100644
--- a/cmd/restore.go
+++ b/cmd/restore.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"math/rand"
 	"os"
+	"runtime"
 	"strconv"
 	"strings"
 	"sync"
@@ -42,7 +43,7 @@ $ juicefs restore redis://localhost/1 2023-05-10-01`,
 
 func restore(ctx *cli.Context) error {
 	setup(ctx, 2)
-	if os.Getuid() != 0 {
+	if os.Getuid() != 0 && runtime.GOOS != "windows" {
 		return fmt.Errorf("only root can restore files from trash")
 	}
 	removePassword(ctx.Args().Get(0))
diff --git a/go.mod b/go.mod
index 4fbb981d..87fcae38 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,8 @@ require (
 	github.com/DataDog/zstd v1.5.6
 	github.com/IBM/ibm-cos-sdk-go v1.12.1
 	github.com/agiledragon/gomonkey/v2 v2.6.0
-	github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
+	github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.1
+	github.com/aliyun/credentials-go v1.4.5
 	github.com/aws/aws-sdk-go v1.55.6
 	github.com/baidubce/bce-sdk-go v0.9.221
 	github.com/ceph/go-ceph v0.18.0
@@ -108,6 +109,8 @@ require (
 	github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
 	github.com/alecthomas/participle v0.2.1 // indirect
 	github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
+	github.com/alibabacloud-go/debug v1.0.1 // indirect
+	github.com/alibabacloud-go/tea v1.2.2 // indirect
 	github.com/andybalholm/brotli v1.1.0 // indirect
 	github.com/apple/foundationdb/bindings/go v0.0.0-20211207225159-47b9a81d1c10
 	github.com/armon/go-metrics v0.3.10 // indirect
@@ -238,7 +241,6 @@ require (
 	github.com/secure-io/sio-go v0.3.1 // indirect
 	github.com/shirou/gopsutil/v3 v3.23.11 // indirect
 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
-	github.com/smartystreets/goconvey v1.7.2 // indirect
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
 	github.com/syndtr/goleveldb v1.0.0 // indirect
 	github.com/tiancaiamao/gp v0.0.0-20221230034425-4025bc8a4d4a // indirect
@@ -279,7 +281,7 @@ require (
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
 	google.golang.org/grpc v1.67.2 // indirect
 	google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
-	gopkg.in/ini.v1 v1.57.0 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 4d369dc7..86cc622d 100644
--- a/go.sum
+++ b/go.sum
@@ -119,10 +119,17 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
 github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
+github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
+github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
+github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
+github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
+github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
 github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
+github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.1 h1:sOhpJdR/+lbQniznp3cYSfwQlXbVkT0ccuiZScBrI6Y=
+github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.1/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
 github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
-github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
-github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
+github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
+github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
 github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
@@ -431,7 +438,6 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC
 github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
 github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
@@ -555,7 +561,6 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/juicedata/cli/v2 v2.19.4-0.20230605075551-9c9c5c0dce83 h1:RyHTka3jCnTaUqfRYjlwcQlr53aasmkvHEbYLXthqr8=
 github.com/juicedata/cli/v2 v2.19.4-0.20230605075551-9c9c5c0dce83/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
@@ -718,6 +723,7 @@ github.com/ncw/directio v1.0.5 h1:JSUBhdjEvVaJvOoyPAbcW0fnd0tvRXD76wEfZ1KcQz4=
 github.com/ncw/directio v1.0.5/go.mod h1:rX/pKEYkOXBGOggmcyJeJGloCkleSvphPx2eV3t6ROk=
 github.com/ncw/swift/v2 v2.0.3 h1:8R9dmgFIWs+RiVlisCEfiQiik1hjuR0JnOkLxaP9ihg=
 github.com/ncw/swift/v2 v2.0.3/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw=
 github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ=
 github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
@@ -877,12 +883,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
-github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
-github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -899,6 +901,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -1064,6 +1067,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
 golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1119,6 +1123,7 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
 golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1198,6 +1203,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
 golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1205,6 +1211,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
 golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
 golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1217,6 +1224,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1313,6 +1321,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
@@ -1323,8 +1332,9 @@ gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
 gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 h1:2TSTkQ8PMvGOD5eeqqRVv6Z9+BYI+bowK97RCr3W+9M=
 gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE=
 gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index f89487b4..3e135d7d 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -129,10 +129,12 @@ type attrCache struct {
 }
 
 type FileSystem struct {
-	conf   *vfs.Config
-	reader vfs.DataReader
-	writer vfs.DataWriter
-	m      meta.Meta
+	conf        *vfs.Config
+	reader      vfs.DataReader
+	writer      vfs.DataWriter
+	m           meta.Meta
+	store       chunk.ChunkStore
+	cacheFiller *vfs.CacheFiller
 
 	cacheM          sync.Mutex
 	entries         map[Ino]map[string]*entryCache
@@ -165,7 +167,9 @@ func NewFileSystem(conf *vfs.Config, m meta.Meta, d chunk.ChunkStore) (*FileSyst
 	reader := vfs.NewDataReader(conf, m, d)
 	fs := &FileSystem{
 		m:               m,
+		store:           d,
 		conf:            conf,
+		cacheFiller:     vfs.NewCacheFiller(conf, m, d),
 		reader:          reader,
 		writer:          vfs.NewDataWriter(conf, m, d, reader),
 		entries:         make(map[meta.Ino]map[string]*entryCache),
@@ -987,6 +991,51 @@ func (fs *FileSystem) Close() error {
 	return nil
 }
 
+func (fs *FileSystem) Clone(ctx meta.Context, src, dst string, preserve bool) (err syscall.Errno) {
+	srcParent, err := fs.resolve(ctx, parentDir(src), true)
+	if err != 0 {
+		return
+	}
+	var srcIno Ino
+	err = fs.lookup(ctx, srcParent.Inode(), path.Base(src), &srcIno, &Attr{})
+	if err != 0 {
+		return
+	}
+	dstParent, err := fs.resolve(ctx, parentDir(dst), true)
+	if err != 0 {
+		return
+	}
+
+	var count, total uint64
+	umask := uint16(utils.GetUmask())
+
+	var cmode uint8
+	if preserve {
+		cmode |= meta.CLONE_MODE_PRESERVE_ATTR
+	}
+
+	if err = fs.m.Clone(meta.NewContext(ctx.Pid(), ctx.Uid(), ctx.Gids()), srcParent.Inode(), srcIno, dstParent.Inode(), path.Base(dst), cmode, umask, &count, &total); err != 0 {
+		logger.Errorf("clone failed srcIno:%d,dstParentIno:%d,dstName:%s,cmode:%d,umask:%d,eno:%v", srcIno, dstParent.Inode(), path.Base(dst), cmode, umask, err)
+	}
+	return
+}
+
+func (fs *FileSystem) Warmup(ctx meta.Context, paths []string, numthreads int, background bool, isEvict bool, isCheck bool, resp *vfs.CacheResponse) {
+	action := vfs.WarmupCache
+	if isEvict {
+		action = vfs.EvictCache
+	}
+	if isCheck {
+		action = vfs.CheckCache
+	}
+
+	if background {
+		go fs.cacheFiller.Cache(meta.NewContext(ctx.Pid(), ctx.Uid(), ctx.Gids()), action, paths, int(numthreads), resp)
+	} else {
+		fs.cacheFiller.Cache(meta.NewContext(ctx.Pid(), ctx.Uid(), ctx.Gids()), action, paths, int(numthreads), resp)
+	}
+}
+
 // File
 
 func (f *File) FS() *FileSystem {
@@ -1326,13 +1375,29 @@ func (f *File) ReaddirPlus(ctx meta.Context, offset int) (entries []*meta.Entry,
 	return
 }
 
-func (f *File) Summary(ctx meta.Context) (s *meta.Summary, err syscall.Errno) {
+func (f *File) Summary(ctx meta.Context, recursive, strict bool) (s *meta.Summary, err syscall.Errno) {
 	defer trace.StartRegion(context.TODO(), "fs.Summary").End()
 	l := vfs.NewLogContext(ctx)
 	defer func() {
 		f.fs.log(l, "Summary (%s): %s (%d,%d,%d,%d)", f.path, errstr(err), s.Length, s.Size, s.Files, s.Dirs)
 	}()
 	s = &meta.Summary{}
-	err = f.fs.m.GetSummary(ctx, f.inode, s, true, true)
+	err = f.fs.m.GetSummary(ctx, f.inode, s, recursive, strict)
+	return
+}
+
+func (f *File) GetTreeSummary(ctx meta.Context, depth, entries uint8, strict bool) (s *meta.TreeSummary, err syscall.Errno) {
+	s = &meta.TreeSummary{
+		Inode: f.inode,
+		Path:  "",
+		Type:  meta.TypeDirectory,
+	}
+
+	l := vfs.NewLogContext(ctx)
+	defer func() {
+		f.fs.log(l, "GetTreeSummary (%s,%d,%d,%t): %s (%d,%d,%d)", f.path, depth, entries, strict, errstr(err), s.Size, s.Files, s.Dirs)
+	}()
+	err = f.fs.m.GetTreeSummary(ctx, s, depth, entries, strict, nil)
+	s.Path = path.Base(f.path)
 	return
 }
diff --git a/pkg/object/oss.go b/pkg/object/oss.go
index bcc3b2ee..4a0ad00a 100644
--- a/pkg/object/oss.go
+++ b/pkg/object/oss.go
@@ -21,187 +21,200 @@ package object
 
 import (
 	"bytes"
-	"encoding/json"
+	"context"
+	"errors"
 	"fmt"
 	"io"
-	"net"
 	"net/http"
 	"net/url"
 	"os"
 	"sort"
-	"strconv"
 	"strings"
 	"time"
 
-	"github.com/aliyun/aliyun-oss-go-sdk/oss"
+	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
+	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
+	openapicred "github.com/aliyun/credentials-go/credentials"
 )
 
 const ossDefaultRegionID = "cn-hangzhou"
 
 type ossClient struct {
 	client *oss.Client
-	bucket *oss.Bucket
+	bucket string
 	sc     string
 }
 
 func (o *ossClient) String() string {
-	return fmt.Sprintf("oss://%s/", o.bucket.BucketName)
+	return fmt.Sprintf("oss://%s/", o.bucket)
 }
 
 func (o *ossClient) Limits() Limits {
 	return Limits{
 		IsSupportMultipartUpload: true,
 		IsSupportUploadPartCopy:  true,
-		MinPartSize:              100 << 10,
-		MaxPartSize:              5 << 30,
-		MaxPartCount:             10000,
+		MinPartSize:              int(oss.MinPartSize),
+		MaxPartSize:              oss.MaxPartSize,
+		MaxPartCount:             int(oss.MaxUploadParts),
 	}
 }
 
 func (o *ossClient) Create() error {
-	var option []oss.Option
+	var configuration *oss.CreateBucketConfiguration
 	if o.sc != "" {
-		option = append(option, oss.StorageClass(oss.StorageClassType(o.sc)))
+		configuration = &oss.CreateBucketConfiguration{
+			StorageClass: oss.StorageClassType(o.sc),
+		}
 	}
-	err := o.bucket.Client.CreateBucket(o.bucket.BucketName, option...)
+	_, err := o.client.PutBucket(ctx, &oss.PutBucketRequest{
+		Bucket:                    &o.bucket,
+		CreateBucketConfiguration: configuration,
+	})
 	if err != nil && isExists(err) {
 		err = nil
 	}
 	return err
 }
 
-func (o *ossClient) checkError(err error) error {
-	if err == nil {
-		return nil
-	}
-	msg := err.Error()
-	if strings.Contains(msg, "InvalidAccessKeyId") || strings.Contains(msg, "SecurityTokenExpired") {
-		logger.Warnf("refresh security token: %s", err)
-		go o.refreshToken()
-	}
-	return err
-}
-
 func (o *ossClient) Head(key string) (Object, error) {
-	var r http.Header
-	var err error
-	if o.sc != "" {
-		r, err = o.bucket.GetObjectDetailedMeta(key)
-	} else {
-		r, err = o.bucket.GetObjectMeta(key)
-	}
-	if o.checkError(err) != nil {
-		if e, ok := err.(oss.ServiceError); ok && e.StatusCode == http.StatusNotFound {
+	info, err := o.client.HeadObject(ctx, &oss.HeadObjectRequest{
+		Bucket: &o.bucket,
+		Key:    &key,
+	})
+	if err != nil {
+		var svcErr *oss.ServiceError
+		if errors.As(err, &svcErr); svcErr.StatusCode == http.StatusNotFound {
 			err = os.ErrNotExist
 		}
 		return nil, err
 	}
-
-	lastModified := r.Get("Last-Modified")
-	if lastModified == "" {
-		return nil, fmt.Errorf("cannot get last modified time")
-	}
-	contentLength := r.Get("Content-Length")
-	mtime, _ := time.Parse(time.RFC1123, lastModified)
-	size, _ := strconv.ParseInt(contentLength, 10, 64)
 	return &obj{
 		key,
-		size,
-		mtime,
+		info.ContentLength,
+		oss.ToTime(info.LastModified),
 		strings.HasSuffix(key, "/"),
-		r.Get(oss.HTTPHeaderOssStorageClass),
+		oss.ToString(info.StorageClass),
 	}, nil
 }
 
 func (o *ossClient) Get(key string, off, limit int64, getters ...AttrGetter) (resp io.ReadCloser, err error) {
-	var respHeader http.Header
-	if off > 0 || limit > 0 {
-		var r string
-		if limit > 0 {
-			r = fmt.Sprintf("%d-%d", off, off+limit-1)
-		} else {
-			r = fmt.Sprintf("%d-", off)
+	var result *oss.GetObjectResult
+	var reqId string
+	var sc string
+	result, err = o.client.GetObject(ctx, &oss.GetObjectRequest{
+		Bucket:        &o.bucket,
+		Key:           &key,
+		Range:         oss.HTTPRange{Offset: off, Count: limit}.FormatHTTPRange(),
+		RangeBehavior: oss.Ptr("standard"),
+	})
+	if err != nil {
+		var svcErr *oss.ServiceError
+		if errors.As(err, &svcErr) {
+			reqId = svcErr.RequestID
 		}
-		resp, err = o.bucket.GetObject(key, oss.NormalizedRange(r), oss.RangeBehavior("standard"), oss.GetResponseHeader(&respHeader))
 	} else {
-		resp, err = o.bucket.GetObject(key, oss.GetResponseHeader(&respHeader))
-		if err == nil {
-			length, err := strconv.ParseInt(resp.(*oss.Response).Headers.Get(oss.HTTPHeaderContentLength), 10, 64)
-			if err != nil {
-				length = -1
-				logger.Warnf("failed to parse content-length %s: %s", resp.(*oss.Response).Headers.Get(oss.HTTPHeaderContentLength), err)
-			}
-			resp = verifyChecksum(resp,
-				resp.(*oss.Response).Headers.Get(oss.HTTPHeaderOssMetaPrefix+checksumAlgr),
-				length)
+		reqId = result.ResultCommon.Headers.Get(oss.HeaderOssRequestID)
+		sc = oss.ToString(result.StorageClass)
+		if off > 0 || limit > 0 {
+			resp = result.Body
+		} else {
+			resp = verifyChecksum(result.Body,
+				result.Headers.Get(oss.HeaderOssMetaPrefix+checksumAlgr),
+				result.ContentLength)
 		}
 	}
+
 	attrs := applyGetters(getters...)
-	attrs.SetRequestID(respHeader.Get(oss.HTTPHeaderOssRequestID))
-	attrs.SetStorageClass(respHeader.Get(oss.HTTPHeaderOssStorageClass))
-	err = o.checkError(err)
+	attrs.SetRequestID(reqId)
+	attrs.SetStorageClass(sc)
 	return
 }
 
 func (o *ossClient) Put(key string, in io.Reader, getters ...AttrGetter) error {
-	var option []oss.Option
+	req := &oss.PutObjectRequest{
+		Bucket:       &o.bucket,
+		Key:          &key,
+		StorageClass: oss.StorageClassType(o.sc),
+		Body:         in,
+	}
 	if ins, ok := in.(io.ReadSeeker); ok {
-		option = append(option, oss.Meta(checksumAlgr, generateChecksum(ins)))
+		req.Metadata = make(map[string]string)
+		req.Metadata[oss.HeaderOssMetaPrefix+checksumAlgr] = generateChecksum(ins)
 	}
-	if o.sc != "" {
-		option = append(option, oss.ObjectStorageClass(oss.StorageClassType(o.sc)))
+	var reqId string
+	result, err := o.client.PutObject(ctx, req)
+	if err != nil {
+		var svcErr *oss.ServiceError
+		if errors.As(err, &svcErr) {
+			reqId = svcErr.RequestID
+		}
+	} else {
+		reqId = result.Headers.Get(oss.HeaderOssRequestID)
 	}
-	var respHeader http.Header
-	option = append(option, oss.GetResponseHeader(&respHeader))
-	err := o.bucket.PutObject(key, in, option...)
 	attrs := applyGetters(getters...)
-	attrs.SetRequestID(respHeader.Get(oss.HTTPHeaderOssRequestID)).SetStorageClass(o.sc)
-	return o.checkError(err)
+	attrs.SetRequestID(reqId).SetStorageClass(o.sc)
+	return err
 }
 
 func (o *ossClient) Copy(dst, src string) error {
-	var option []oss.Option
-	if o.sc != "" {
-		option = append(option, oss.ObjectStorageClass(oss.StorageClassType(o.sc)))
-	}
-	_, err := o.bucket.CopyObject(src, dst, option...)
-	return o.checkError(err)
+	var req = &oss.CopyObjectRequest{
+		SourceBucket: &o.bucket,
+		Bucket:       &o.bucket,
+		SourceKey:    &src,
+		Key:          &dst,
+		StorageClass: oss.StorageClassType(o.sc),
+	}
+	_, err := o.client.CopyObject(ctx, req)
+	return err
 }
 
 func (o *ossClient) Delete(key string, getters ...AttrGetter) error {
-	var respHeader http.Header
-	err := o.bucket.DeleteObject(key, oss.GetResponseHeader(&respHeader))
+	result, err := o.client.DeleteObject(ctx, &oss.DeleteObjectRequest{
+		Bucket: &o.bucket,
+		Key:    &key,
+	})
+	var reqId string
+	if err != nil {
+		var svcErr *oss.ServiceError
+		if errors.As(err, &svcErr) {
+			reqId = svcErr.RequestID
+		}
+	} else {
+		reqId = result.Headers.Get(oss.HeaderOssRequestID)
+	}
 	attrs := applyGetters(getters...)
-	attrs.SetRequestID(respHeader.Get(oss.HTTPHeaderOssRequestID))
-	return o.checkError(err)
+	attrs.SetRequestID(reqId)
+	return err
 }
 
 func (o *ossClient) List(prefix, start, token, delimiter string, limit int64, followLink bool) ([]Object, bool, string, error) {
 	if limit > 1000 {
 		limit = 1000
 	}
-	result, err := o.bucket.ListObjectsV2(
-		oss.Prefix(prefix),
-		oss.StartAfter(start),
-		oss.ContinuationToken(token),
-		oss.Delimiter(delimiter),
-		oss.MaxKeys(int(limit)))
-	if o.checkError(err) != nil {
+	result, err := o.client.ListObjectsV2(ctx, &oss.ListObjectsV2Request{
+		Bucket:            &o.bucket,
+		Prefix:            &prefix,
+		StartAfter:        &start,
+		ContinuationToken: &token,
+		Delimiter:         &delimiter,
+		MaxKeys:           int32(limit),
+	})
+	if err != nil {
 		return nil, false, "", err
 	}
-	n := len(result.Objects)
+	n := len(result.Contents)
 	objs := make([]Object, n)
 	for i := 0; i < n; i++ {
-		o := result.Objects[i]
-		objs[i] = &obj{o.Key, o.Size, o.LastModified, strings.HasSuffix(o.Key, "/"), o.StorageClass}
+		o := result.Contents[i]
+		objs[i] = &obj{oss.ToString(o.Key), o.Size, oss.ToTime(o.LastModified), strings.HasSuffix(oss.ToString(o.Key), "/"), oss.ToString(o.StorageClass)}
 	}
 	if delimiter != "" {
 		for _, o := range result.CommonPrefixes {
-			objs = append(objs, &obj{o, 0, time.Unix(0, 0), true, ""})
+			objs = append(objs, &obj{oss.ToString(o.Prefix), 0, time.Unix(0, 0), true, ""})
 		}
 		sort.Slice(objs, func(i, j int) bool { return objs[i].Key() < objs[j].Key() })
 	}
-	return objs, result.IsTruncated, result.NextContinuationToken, nil
+	return objs, result.IsTruncated, oss.ToString(result.NextContinuationToken), nil
 }
 
 func (o *ossClient) ListAll(prefix, marker string, followLink bool) (<-chan Object, error) {
@@ -209,70 +222,85 @@ func (o *ossClient) ListAll(prefix, marker string, followLink bool) (<-chan Obje
 }
 
 func (o *ossClient) CreateMultipartUpload(key string) (*MultipartUpload, error) {
-	var option []oss.Option
-	if o.sc != "" {
-		option = append(option, oss.ObjectStorageClass(oss.StorageClassType(o.sc)))
-	}
-	r, err := o.bucket.InitiateMultipartUpload(key, option...)
-	if o.checkError(err) != nil {
+	result, err := o.client.InitiateMultipartUpload(ctx, &oss.InitiateMultipartUploadRequest{
+		Bucket:       &o.bucket,
+		Key:          &key,
+		StorageClass: oss.StorageClassType(o.sc),
+	})
+	if err != nil {
 		return nil, err
 	}
-	return &MultipartUpload{UploadID: r.UploadID, MinPartSize: 4 << 20, MaxCount: 10000}, nil
+	return &MultipartUpload{UploadID: oss.ToString(result.UploadId), MinPartSize: 4 << 20, MaxCount: 10000}, nil
 }
 
 func (o *ossClient) UploadPart(key string, uploadID string, num int, data []byte) (*Part, error) {
-	initResult := oss.InitiateMultipartUploadResult{
-		Key:      key,
-		UploadID: uploadID,
-	}
-	r, err := o.bucket.UploadPart(initResult, bytes.NewReader(data), int64(len(data)), num)
-	if o.checkError(err) != nil {
+	r, err := o.client.UploadPart(ctx, &oss.UploadPartRequest{
+		Bucket:     &o.bucket,
+		UploadId:   &uploadID,
+		Key:        &key,
+		Body:       bytes.NewReader(data),
+		PartNumber: int32(num),
+	})
+	if err != nil {
 		return nil, err
 	}
-	return &Part{Num: num, ETag: r.ETag}, nil
+	return &Part{Num: num, ETag: oss.ToString(r.ETag)}, nil
 }
 
 func (o *ossClient) UploadPartCopy(key string, uploadID string, num int, srcKey string, off, size int64) (*Part, error) {
-	initMultipartResult := oss.InitiateMultipartUploadResult{Bucket: o.bucket.BucketName, Key: key, UploadID: uploadID}
-	partCopy, err := o.bucket.UploadPartCopy(initMultipartResult, o.bucket.BucketName, srcKey, off, size, num)
-	if o.checkError(err) != nil {
+	partCopy, err := o.client.UploadPartCopy(ctx, &oss.UploadPartCopyRequest{
+		SourceBucket: &o.bucket,
+		Bucket:       &o.bucket,
+		SourceKey:    &srcKey,
+		Key:          &key,
+		UploadId:     &uploadID,
+		PartNumber:   int32(num),
+		Range:        oss.HTTPRange{Offset: off, Count: size}.FormatHTTPRange(),
+	})
+	if err != nil {
 		return nil, err
 	}
-	return &Part{Num: num, ETag: partCopy.ETag}, nil
+	return &Part{Num: num, ETag: oss.ToString(partCopy.ETag)}, nil
 }
 
 func (o *ossClient) AbortUpload(key string, uploadID string) {
-	initResult := oss.InitiateMultipartUploadResult{
-		Key:      key,
-		UploadID: uploadID,
-	}
-	_ = o.bucket.AbortMultipartUpload(initResult)
+	_, _ = o.client.AbortMultipartUpload(ctx, &oss.AbortMultipartUploadRequest{
+		Bucket:   &o.bucket,
+		UploadId: &uploadID,
+		Key:      &key,
+	})
 }
 
 func (o *ossClient) CompleteUpload(key string, uploadID string, parts []*Part) error {
-	initResult := oss.InitiateMultipartUploadResult{
-		Key:      key,
-		UploadID: uploadID,
-	}
 	oparts := make([]oss.UploadPart, len(parts))
 	for i, p := range parts {
-		oparts[i].PartNumber = p.Num
-		oparts[i].ETag = p.ETag
-	}
-	_, err := o.bucket.CompleteMultipartUpload(initResult, oparts)
-	return o.checkError(err)
+		oparts[i].PartNumber = int32(p.Num)
+		oparts[i].ETag = &p.ETag
+	}
+	_, err := o.client.CompleteMultipartUpload(ctx, &oss.CompleteMultipartUploadRequest{
+		Bucket:   &o.bucket,
+		Key:      &key,
+		UploadId: &uploadID,
+		CompleteMultipartUpload: &oss.CompleteMultipartUpload{
+			Parts: oparts,
+		},
+	})
+	return err
 }
 
 func (o *ossClient) ListUploads(marker string) ([]*PendingPart, string, error) {
-	result, err := o.bucket.ListMultipartUploads(oss.KeyMarker(marker))
-	if o.checkError(err) != nil {
+	result, err := o.client.ListParts(ctx, &oss.ListPartsRequest{
+		Bucket: &o.bucket,
+		Key:    &marker,
+	})
+	if err != nil {
 		return nil, "", err
 	}
-	parts := make([]*PendingPart, len(result.Uploads))
-	for i, u := range result.Uploads {
-		parts[i] = &PendingPart{u.Key, u.UploadID, u.Initiated}
+	parts := make([]*PendingPart, len(result.Parts))
+	for i, u := range result.Parts {
+		parts[i] = &PendingPart{oss.ToString(result.Key), oss.ToString(result.UploadId), oss.ToTime(u.LastModified)}
 	}
-	return parts, result.NextKeyMarker, nil
+	return parts, string(result.NextPartNumberMarker), nil
 }
 
 func (o *ossClient) SetStorageClass(sc string) error {
@@ -280,125 +308,32 @@ func (o *ossClient) SetStorageClass(sc string) error {
 	return nil
 }
 
-type stsCred struct {
-	AccessKeyId     string
-	AccessKeySecret string
-	Expiration      string
-	SecurityToken   string
-	LastUpdated     string
-	Code            string
-}
-
-func fetch(url string) ([]byte, error) {
-	req, err := http.NewRequest("GET", url, nil)
-	if err != nil {
-		return nil, err
-	}
-	resp, err := httpClient.Do(req)
-	if err != nil {
-		return nil, err
-	}
-	return io.ReadAll(resp.Body)
-}
-
-func fetchStsToken() (*stsCred, error) {
-	if cred, err := fetchStsCred(); err == nil {
-		return cred, nil
-	}
-
-	// EMR MetaService: https://help.aliyun.com/document_detail/43966.html
-	url := "http://127.0.0.1:10011/"
-	token, err := fetch(url + "role-security-token")
-	if err != nil {
-		return nil, err
-	}
-	accessKey, err := fetch(url + "role-access-key-id")
-	if err != nil {
-		return nil, err
-	}
-	secretKey, err := fetch(url + "role-access-key-secret")
-	if err != nil {
-		return nil, err
-	}
-	return &stsCred{
-		SecurityToken:   string(token),
-		AccessKeyId:     string(accessKey),
-		AccessKeySecret: string(secretKey),
-		Expiration:      time.Now().Add(time.Hour * 24 * 100).Format("2006-01-02T15:04:05Z"),
-	}, nil
-}
-
-func fetchStsCred() (*stsCred, error) {
-	url := "http://100.100.100.200/latest/meta-data/Ram/security-credentials/"
-	role, err := fetch(url)
-	if err != nil {
-		return nil, err
-	}
-	d, err := fetch(url + string(role))
-	if err != nil {
-		return nil, err
-	}
-	var cred stsCred
-	err = json.Unmarshal(d, &cred)
-	return &cred, err
-}
-
-func (o *ossClient) refreshToken() time.Time {
-	cred, err := fetchStsToken()
-	if err != nil {
-		logger.Errorf("refresh token: %s", err)
-		return time.Now().Add(time.Second)
-	}
-	o.client.Config.AccessKeyID = cred.AccessKeyId
-	o.client.Config.AccessKeySecret = cred.AccessKeySecret
-	o.client.Config.SecurityToken = cred.SecurityToken
-	logger.Debugf("Refreshed STS, will be expired at %s", cred.Expiration)
-	expire, err := time.Parse("2006-01-02T15:04:05Z", cred.Expiration)
-	if err != nil {
-		logger.Errorf("invalid expiration: %s, %s", cred.Expiration, err)
-		return time.Now().Add(time.Minute)
-	}
-	return expire
-}
-
-func autoOSSEndpoint(bucketName, accessKey, secretKey, securityToken string) (string, error) {
-	var client *oss.Client
+func autoOSSEndpoint(bucketName string, provider credentials.CredentialsProvider) (string, error) {
 	var err error
-
 	regionID := ossDefaultRegionID
 	if rid := os.Getenv("ALICLOUD_REGION_ID"); rid != "" {
 		regionID = rid
 	}
-	defaultEndpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", regionID)
-
-	if securityToken == "" {
-		if client, err = oss.New(defaultEndpoint, accessKey, secretKey); err != nil {
-			return "", err
-		}
-	} else {
-		if client, err = oss.New(defaultEndpoint, accessKey, secretKey,
-			oss.SecurityToken(securityToken)); err != nil {
-			return "", err
-		}
-	}
-
-	result, err := client.ListBuckets(oss.Prefix(bucketName), oss.MaxKeys(1))
+	config := oss.NewConfig()
+	config.CredentialsProvider = provider
+	config.Region = &regionID
+	client := oss.NewClient(config)
+	var info *oss.GetBucketInfoResult
+	info, err = client.GetBucketInfo(ctx, &oss.GetBucketInfoRequest{
+		Bucket: &bucketName,
+	})
 	if err != nil {
 		return "", err
 	}
-	if len(result.Buckets) == 0 {
-		return "", fmt.Errorf("cannot list bucket %q using endpoint %q", bucketName, defaultEndpoint)
-	}
-
-	bucketLocation := result.Buckets[0].Location
 	// try oss internal endpoint
-	if conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s-internal.aliyuncs.com:http",
-		bucketLocation), time.Second*3); err == nil {
-		_ = conn.Close()
-		return fmt.Sprintf("http://%s-internal.aliyuncs.com", bucketLocation), nil
-	}
-
-	return fmt.Sprintf("https://%s.aliyuncs.com", bucketLocation), nil
+	client2 := oss.NewClient(oss.NewConfig().
+		WithEndpoint(oss.ToString(info.BucketInfo.IntranetEndpoint)).
+		WithCredentialsProvider(provider).
+		WithRegion(regionID))
+	if _, err := client2.GetBucketInfo(ctx, &oss.GetBucketInfoRequest{Bucket: &bucketName}); err == nil {
+		return "http://" + oss.ToString(info.BucketInfo.IntranetEndpoint), err
+	}
+	return "https://" + oss.ToString(info.BucketInfo.ExtranetEndpoint), nil
 }
 
 func newOSS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
@@ -407,7 +342,7 @@ func newOSS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error)
 	}
 	uri, err := url.ParseRequestURI(endpoint)
 	if err != nil {
-		return nil, fmt.Errorf("Invalid endpoint: %v, error: %v", endpoint, err)
+		return nil, fmt.Errorf("invalid endpoint: %v, error: %v", endpoint, err)
 	}
 	hostParts := strings.SplitN(uri.Host, ".", 2)
 	bucketName := hostParts[0]
@@ -416,71 +351,60 @@ func newOSS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error)
 	if len(hostParts) > 1 {
 		domain = uri.Scheme + "://" + hostParts[1]
 	}
-
-	var refresh bool
+	// try environment variable
 	if accessKey == "" {
-		// try environment variable
 		accessKey = os.Getenv("ALICLOUD_ACCESS_KEY_ID")
 		secretKey = os.Getenv("ALICLOUD_ACCESS_KEY_SECRET")
 		token = os.Getenv("SECURITY_TOKEN")
-
-		if accessKey == "" {
-			var err error
-			var cred *stsCred
-			maxRetry := 4
-			for i := 0; i < maxRetry; i++ {
-				time.Sleep(time.Second * time.Duration(i))
-				if cred, err = fetchStsToken(); err != nil {
-					logger.Warnf("Fetch STS Token try %d: %s", i+1, err)
-				} else {
-					accessKey = cred.AccessKeyId
-					secretKey = cred.AccessKeySecret
-					token = cred.SecurityToken
-					refresh = true
-					break
-				}
-			}
+	}
+	var provider credentials.CredentialsProvider
+	if accessKey == "" {
+		// use default credential chain https://github.com/aliyun/credentials-go?tab=readme-ov-file#credential-provider-chain
+		defaultCred, _ := openapicred.NewCredential(nil)
+		provider = credentials.CredentialsProviderFunc(func(ctx context.Context) (credentials.Credentials, error) {
+			// return the old certificate before its expiration and obtain a new certificate when the old certificate expires
+			cred, err := defaultCred.GetCredential()
 			if err != nil {
-				return nil, fmt.Errorf("No credential provided for OSS: %s", err)
+				return credentials.Credentials{}, err
 			}
-		}
+			return credentials.Credentials{
+				AccessKeyID:     *cred.AccessKeyId,
+				AccessKeySecret: *cred.AccessKeySecret,
+				SecurityToken:   *cred.SecurityToken,
+			}, nil
+		})
+	} else {
+		provider = credentials.NewStaticCredentialsProvider(accessKey, secretKey, token)
 	}
 
 	if domain == "" {
-		if domain, err = autoOSSEndpoint(bucketName, accessKey, secretKey, token); err != nil {
-			return nil, fmt.Errorf("Unable to get endpoint of bucket %s: %s", bucketName, err)
+		if domain, err = autoOSSEndpoint(bucketName, provider); err != nil {
+			return nil, fmt.Errorf("unable to get endpoint of bucket %s: %s", bucketName, err)
 		}
-		logger.Debugf("Use endpoint %q", domain)
-	}
-
-	client, err := oss.New(domain, accessKey, secretKey, oss.SecurityToken(token), oss.HTTPClient(httpClient))
-	if err != nil {
-		return nil, fmt.Errorf("Cannot create OSS client with endpoint %s: %s", endpoint, err)
-	}
-
-	client.Config.Timeout = 10
-	client.Config.RetryTimes = 1
-	client.Config.HTTPTimeout.ConnectTimeout = time.Second * 2   // 30s
-	client.Config.HTTPTimeout.ReadWriteTimeout = time.Second * 5 // 60s
-	client.Config.HTTPTimeout.HeaderTimeout = time.Second * 5    // 60s
-	client.Config.HTTPTimeout.LongTimeout = time.Second * 30     // 300s
-	client.Config.IsEnableCRC = false                            // CRC64ECMA is much slower than CRC32C
-	client.Config.UserAgent = UserAgent
-
-	bucket, err := client.Bucket(bucketName)
-	if err != nil {
-		return nil, fmt.Errorf("Cannot create bucket %s: %s", bucketName, err)
-	}
-
-	o := &ossClient{client: client, bucket: bucket}
-	if token != "" && refresh {
-		go func() {
-			for {
-				next := o.refreshToken()
-				time.Sleep(time.Until(next) / 2)
-			}
-		}()
-	}
+		logger.Debugf("use endpoint %s", domain)
+	}
+	index := strings.Index(domain, ".")
+	if index <= 0 {
+		return nil, fmt.Errorf("invalid endpoint: %s", domain)
+	}
+	_, regionID, found := strings.Cut(domain[:index], "oss-")
+	if !found {
+		return nil, fmt.Errorf("invalid endpoint: %s", domain)
+	}
+	regionID, _ = strings.CutSuffix(regionID, "-internal")
+	config := oss.LoadDefaultConfig()
+	config.Endpoint = oss.Ptr(domain)
+	config.Region = oss.Ptr(regionID)
+	config.RetryMaxAttempts = oss.Ptr(1)
+	config.ConnectTimeout = oss.Ptr(time.Second * 2)
+	config.ReadWriteTimeout = oss.Ptr(time.Second * 5)
+	config.DisableUploadCRC64Check = oss.Ptr(true)
+	config.DisableDownloadCRC64Check = oss.Ptr(true)
+	config.UserAgent = &UserAgent
+	config.HttpClient = httpClient
+	config.CredentialsProvider = provider
+	client := oss.NewClient(config)
+	o := &ossClient{client: client, bucket: bucketName}
 	return o, nil
 }
 
diff --git a/pkg/vfs/backup.go b/pkg/vfs/backup.go
index 70382823..1023d26b 100644
--- a/pkg/vfs/backup.go
+++ b/pkg/vfs/backup.go
@@ -22,6 +22,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"runtime"
 	"sort"
 	"strings"
 	"syscall"
@@ -101,7 +102,7 @@ func backup(m meta.Meta, blob object.ObjectStorage, now time.Time, fast, skipTra
 		localDir += "/"
 	}
 	fp, err := os.Create(filepath.Join(localDir, "meta", name))
-	if errors.Is(err, syscall.ENOENT) {
+	if errors.Is(err, syscall.ENOENT) || (errors.Is(err, syscall.ENOTDIR) && runtime.GOOS == "windows") {
 		if err = os.MkdirAll(filepath.Join(localDir, "meta"), 0755); err != nil {
 			return "", err
 		}
diff --git a/pkg/vfs/fill.go b/pkg/vfs/fill.go
index d1edfe93..cc4082e8 100644
--- a/pkg/vfs/fill.go
+++ b/pkg/vfs/fill.go
@@ -18,6 +18,7 @@ package vfs
 
 import (
 	"fmt"
+	"github.com/juicedata/juicefs/pkg/chunk"
 	"path"
 	"strconv"
 	"strings"
@@ -55,7 +56,21 @@ const (
 	CheckCache = 2
 )
 
-func (v *VFS) cache(ctx meta.Context, action CacheAction, paths []string, concurrent int, resp *CacheResponse) {
+type CacheFiller struct {
+	conf  *Config
+	meta  meta.Meta
+	store chunk.ChunkStore
+}
+
+func NewCacheFiller(conf *Config, meta meta.Meta, store chunk.ChunkStore) *CacheFiller {
+	return &CacheFiller{
+		conf:  conf,
+		meta:  meta,
+		store: store,
+	}
+}
+
+func (c *CacheFiller) Cache(ctx meta.Context, action CacheAction, paths []string, concurrent int, resp *CacheResponse) {
 	logger.Infof("start to %s %d paths with %d workers", action, len(paths), concurrent)
 
 	if resp == nil {
@@ -82,18 +97,18 @@ func (v *VFS) cache(ctx meta.Context, action CacheAction, paths []string, concur
 				switch action {
 				case WarmupCache:
 					handler = func(s meta.Slice) error {
-						return v.Store.FillCache(s.Id, s.Size)
+						return c.store.FillCache(s.Id, s.Size)
 					}
 
-					if v.Conf.Meta.OpenCache > 0 {
-						if err := v.Meta.Open(ctx, f.ino, syscall.O_RDONLY, &meta.Attr{}); err != 0 {
+					if c.conf.Meta.OpenCache > 0 {
+						if err := c.meta.Open(ctx, f.ino, syscall.O_RDONLY, &meta.Attr{}); err != 0 {
 							logger.Errorf("Inode %d could be opened: %s", f.ino, err)
 						}
-						_ = v.Meta.Close(ctx, f.ino)
+						_ = c.meta.Close(ctx, f.ino)
 					}
 				case EvictCache:
 					handler = func(s meta.Slice) error {
-						return v.Store.EvictCache(s.Id, s.Size)
+						return c.store.EvictCache(s.Id, s.Size)
 					}
 				case CheckCache:
 					blockHandler := func(exists bool, loc string, size int) {
@@ -106,11 +121,11 @@ func (v *VFS) cache(ctx meta.Context, action CacheAction, paths []string, concur
 						}
 					}
 					handler = func(s meta.Slice) error {
-						return v.Store.CheckCache(s.Id, s.Size, blockHandler)
+						return c.store.CheckCache(s.Id, s.Size, blockHandler)
 					}
 				}
 
-				iter := newSliceIterator(ctx, v.Meta, f.ino, f.size, resp)
+				iter := newSliceIterator(ctx, c.meta, f.ino, f.size, resp)
 				err := iter.Iterate(handler, concurrent)
 				if err != nil {
 					logger.Errorf("%s error : %s", action, err)
@@ -124,13 +139,13 @@ func (v *VFS) cache(ctx meta.Context, action CacheAction, paths []string, concur
 	var inode Ino
 	var attr = &Attr{}
 	for _, p := range paths {
-		if st := v.resolve(ctx, p, &inode, attr); st != 0 {
+		if st := c.resolve(ctx, p, &inode, attr); st != 0 {
 			logger.Warnf("Failed to resolve path %s: %s", p, st)
 			continue
 		}
 		logger.Debugf("path %s", p)
 		if attr.Typ == meta.TypeDirectory {
-			v.walkDir(ctx, inode, todo)
+			c.walkDir(ctx, inode, todo)
 		} else if attr.Typ == meta.TypeFile {
 			_ = sendFile(ctx, todo, _file{inode, attr.Length})
 		}
@@ -156,22 +171,22 @@ func sendFile(ctx meta.Context, todo chan _file, f _file) error {
 	}
 }
 
-func (v *VFS) resolve(ctx meta.Context, p string, inode *Ino, attr *Attr) syscall.Errno {
+func (c *CacheFiller) resolve(ctx meta.Context, p string, inode *Ino, attr *Attr) syscall.Errno {
 	var inodePrefix = "inode:"
 	if strings.HasPrefix(p, inodePrefix) {
 		i, err := strconv.ParseUint(p[len(inodePrefix):], 10, 64)
 		if err == nil {
 			*inode = meta.Ino(i)
-			return v.Meta.GetAttr(ctx, meta.Ino(i), attr)
+			return c.meta.GetAttr(ctx, meta.Ino(i), attr)
 		}
 	}
 	p = strings.Trim(p, "/")
-	err := v.Meta.Resolve(ctx, 1, p, inode, attr)
+	err := c.meta.Resolve(ctx, 1, p, inode, attr)
 	if err != syscall.ENOTSUP {
 		return err
 	}
 
-	// Fallback to the default implementation that calls `m.Lookup` for each directory along the path.
+	// Fallback to the default implementation that calls `meta.Lookup` for each directory along the path.
 	// It might be slower for deep directories, but it works for every meta that implements `Lookup`.
 	parent := Ino(1)
 	ss := strings.Split(p, "/")
@@ -185,16 +200,16 @@ func (v *VFS) resolve(ctx meta.Context, p string, inode *Ino, attr *Attr) syscal
 			break
 		}
 		if i > 0 {
-			if err = v.Meta.Access(ctx, parent, MODE_MASK_R|MODE_MASK_X, attr); err != 0 {
+			if err = c.meta.Access(ctx, parent, MODE_MASK_R|MODE_MASK_X, attr); err != 0 {
 				return err
 			}
 		}
-		if err = v.Meta.Lookup(ctx, parent, name, inode, attr, false); err != 0 {
+		if err = c.meta.Lookup(ctx, parent, name, inode, attr, false); err != 0 {
 			return err
 		}
 		if attr.Typ == meta.TypeSymlink {
 			var buf []byte
-			if err = v.Meta.ReadLink(ctx, *inode, &buf); err != 0 {
+			if err = c.meta.ReadLink(ctx, *inode, &buf); err != 0 {
 				return err
 			}
 			target := string(buf)
@@ -202,7 +217,7 @@ func (v *VFS) resolve(ctx meta.Context, p string, inode *Ino, attr *Attr) syscal
 				return syscall.ENOTSUP
 			}
 			target = path.Join(strings.Join(ss[:i], "/"), target)
-			if err = v.resolve(ctx, target, inode, attr); err != 0 {
+			if err = c.resolve(ctx, target, inode, attr); err != 0 {
 				return err
 			}
 		}
@@ -210,14 +225,14 @@ func (v *VFS) resolve(ctx meta.Context, p string, inode *Ino, attr *Attr) syscal
 	}
 	if parent == meta.RootInode {
 		*inode = parent
-		if err = v.Meta.GetAttr(ctx, *inode, attr); err != 0 {
+		if err = c.meta.GetAttr(ctx, *inode, attr); err != 0 {
 			return err
 		}
 	}
 	return 0
 }
 
-func (v *VFS) walkDir(ctx meta.Context, inode Ino, todo chan _file) {
+func (c *CacheFiller) walkDir(ctx meta.Context, inode Ino, todo chan _file) {
 	pending := make([]Ino, 1)
 	pending[0] = inode
 	for len(pending) > 0 {
@@ -226,7 +241,7 @@ func (v *VFS) walkDir(ctx meta.Context, inode Ino, todo chan _file) {
 		inode = pending[l]
 		pending = pending[:l]
 		var entries []*meta.Entry
-		r := v.Meta.Readdir(ctx, inode, 1, &entries)
+		r := c.meta.Readdir(ctx, inode, 1, &entries)
 		if r == 0 {
 			for _, f := range entries {
 				name := string(f.Name)
diff --git a/pkg/vfs/internal.go b/pkg/vfs/internal.go
index a17fec46..bfac501e 100644
--- a/pkg/vfs/internal.go
+++ b/pkg/vfs/internal.go
@@ -547,12 +547,12 @@ func (v *VFS) handleInternalMsg(ctx meta.Context, cmd uint32, r *utils.Buffer, o
 		if background == 0 {
 			done := make(chan struct{})
 			go func() {
-				v.cache(ctx, action, paths, int(concurrent), stat)
+				v.cacheFiller.Cache(ctx, action, paths, int(concurrent), stat)
 				close(done)
 			}()
 			writeProgress(&stat.FileCount, &stat.TotalBytes, out, done)
 		} else {
-			go v.cache(meta.NewContext(ctx.Pid(), ctx.Uid(), ctx.Gids()), action, paths, int(concurrent), nil)
+			go v.cacheFiller.Cache(meta.NewContext(ctx.Pid(), ctx.Uid(), ctx.Gids()), action, paths, int(concurrent), nil)
 		}
 		data, err := json.Marshal(stat)
 		if err != nil {
diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go
index 8403f428..80503bff 100644
--- a/pkg/vfs/vfs.go
+++ b/pkg/vfs/vfs.go
@@ -1220,6 +1220,7 @@ type VFS struct {
 	UpdateFormat    func(*meta.Format)
 	reader          DataReader
 	writer          DataWriter
+	cacheFiller     *CacheFiller
 
 	handles   map[Ino][]*handle
 	handleIno map[uint64]Ino
@@ -1237,16 +1238,17 @@ func NewVFS(conf *Config, m meta.Meta, store chunk.ChunkStore, registerer promet
 	writer := NewDataWriter(conf, m, store, reader)
 
 	v := &VFS{
-		Conf:       conf,
-		Meta:       m,
-		Store:      store,
-		reader:     reader,
-		writer:     writer,
-		handles:    make(map[Ino][]*handle),
-		handleIno:  make(map[uint64]Ino),
-		modifiedAt: make(map[meta.Ino]time.Time),
-		nextfh:     1,
-		registry:   registry,
+		Conf:        conf,
+		Meta:        m,
+		Store:       store,
+		reader:      reader,
+		writer:      writer,
+		cacheFiller: NewCacheFiller(conf, m, store),
+		handles:     make(map[Ino][]*handle),
+		handleIno:   make(map[uint64]Ino),
+		modifiedAt:  make(map[meta.Ino]time.Time),
+		nextfh:      1,
+		registry:    registry,
 	}
 
 	n := getInternalNode(configInode)
diff --git a/pkg/winfsp/winfs.go b/pkg/winfsp/winfs.go
index 28a8a3d5..c544dc6b 100644
--- a/pkg/winfsp/winfs.go
+++ b/pkg/winfsp/winfs.go
@@ -761,7 +761,7 @@ func Serve(v *vfs.VFS, fuseOpt string, fileCacheTo float64, asRoot bool, delayCl
 	_ = host.Mount(conf.Meta.MountPoint, []string{"-o", options})
 }
 
-func RunAsSystemSerivce(name string, mountpoint string) error {
+func RunAsSystemSerivce(name string, mountpoint string, logPath string) error {
 	// https://winfsp.dev/doc/WinFsp-Service-Architecture/
 	logger.Info("Running as Windows system service.")
 
@@ -816,6 +816,19 @@ func RunAsSystemSerivce(name string, mountpoint string) error {
 		return fmt.Errorf("Failed to set registry key: %s", err)
 	}
 
+	if logPath != "" {
+		err = k.SetStringValue("Stderr", logPath)
+		if err != nil {
+			return fmt.Errorf("Failed to set registry key: %s", err)
+		}
+	} else {
+		err = k.DeleteValue("Stderr")
+		if err != nil {
+			return fmt.Errorf("Failed to delete registry key: %s", err)
+		}
+
+	}
+
 	logger.Debug("Starting juicefs service.")
 	cmd := exec.Command("net", "use", mountpoint, "\\\\juicefs\\"+name)
 	err = cmd.Run()
diff --git a/sdk/java/libjfs/main.go b/sdk/java/libjfs/main.go
index 1d0f4ab1..17cc2182 100644
--- a/sdk/java/libjfs/main.go
+++ b/sdk/java/libjfs/main.go
@@ -1261,7 +1261,7 @@ func jfs_summary(pid int64, h int64, cpath *C.char, buf uintptr) int32 {
 		return errno(err)
 	}
 	defer f.Close(ctx)
-	summary, err := f.Summary(ctx)
+	summary, err := f.Summary(ctx, true, true)
 	if err != 0 {
 		return errno(err)
 	}
@@ -1272,6 +1272,65 @@ func jfs_summary(pid int64, h int64, cpath *C.char, buf uintptr) int32 {
 	return 24
 }
 
+//export jfs_info
+func jfs_info(pid int64, h int64, cpath *C.char, p_buf **byte, recursive, strict bool) int32 {
+	w := F(h)
+	if w == nil {
+		return EINVAL
+	}
+	ctx := w.withPid(pid)
+	f, err := w.Open(ctx, C.GoString(cpath), 0)
+	if err != 0 {
+		return errno(err)
+	}
+	defer f.Close(ctx)
+	info, err := f.Summary(ctx, recursive, strict)
+	if err != 0 {
+		return errno(err)
+	}
+	res, err2 := json.Marshal(info)
+	if err2 != nil {
+		return EINVAL
+	}
+	if *p_buf != nil {
+		return EINVAL
+	}
+
+	*p_buf = (*byte)(C.malloc(C.size_t(len(res))))
+
+	buf := unsafe.Slice(*p_buf, len(res))
+	return int32(copy(buf, res))
+}
+
+//export jfs_gettreesummary
+func jfs_gettreesummary(pid, h int64, cpath *C.char, depth, entries uint8, p_buf **byte) int32 {
+	w := F(h)
+	if w == nil {
+		return EINVAL
+	}
+	ctx := w.withPid(pid)
+	f, err := w.Open(ctx, C.GoString(cpath), 0)
+	if err != 0 {
+		return errno(err)
+	}
+	summary, err := f.GetTreeSummary(ctx, depth, entries, true)
+	if err != 0 {
+		return errno(err)
+	}
+	res, err2 := json.Marshal(summary)
+	if err2 != nil {
+		return EINVAL
+	}
+	if *p_buf != nil {
+		return EINVAL
+	}
+
+	*p_buf = (*byte)(C.malloc(C.size_t(len(res))))
+
+	buf := unsafe.Slice(*p_buf, len(res))
+	return int32(copy(buf, res))
+}
+
 //export jfs_statvfs
 func jfs_statvfs(pid int64, h int64, buf uintptr) int32 {
 	w := F(h)
@@ -1529,10 +1588,8 @@ func jfs_concat(pid int64, h int64, _dst *C.char, buf uintptr, bufsize int32) in
 	return r
 }
 
-// TODO: implement real clone
-
 //export jfs_clone
-func jfs_clone(pid int64, h int64, _src *C.char, _dst *C.char) int32 {
+func jfs_clone(pid int64, h int64, _src *C.char, _dst *C.char, preserve bool) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1540,22 +1597,7 @@ func jfs_clone(pid int64, h int64, _src *C.char, _dst *C.char) int32 {
 	src := C.GoString(_src)
 	dst := C.GoString(_dst)
 	ctx := w.withPid(pid)
-	fi, err := w.Open(ctx, src, 0)
-	if err != 0 {
-		logger.Errorf("open %s: %s", src, err)
-		return errno(err)
-	}
-	defer fi.Close(ctx)
-	fo, err := w.Create(ctx, dst, 0666, 022)
-	if err != 0 {
-		logger.Errorf("create %s: %s", dst, err)
-		return errno(err)
-	}
-	defer fo.Close(ctx)
-	_, err = w.CopyFileRange(ctx, src, 0, dst, 0, 1<<63)
-	if err != 0 {
-		logger.Errorf("copy %s to %s: %s", src, dst, err)
-	}
+	err := w.Clone(ctx, src, dst, preserve)
 	return errno(err)
 }
 
@@ -1698,5 +1740,33 @@ func jfs_close(pid int64, fd int32) int32 {
 	return errno(f.Close(f.w.withPid(pid)))
 }
 
+//export jfs_warmup
+func jfs_warmup(pid int64, h int64, _paths *C.char, numthreads int32, background, isEvict, isCheck bool, p_buf **byte) int32 {
+	resp := &vfs.CacheResponse{Locations: make(map[string]uint64)}
+
+	w := F(h)
+	if w == nil {
+		return EINVAL
+	}
+	ctx := w.withPid(pid)
+
+	var paths []string
+	err := json.Unmarshal([]byte(C.GoString(_paths)), &paths)
+	if err != nil {
+		logger.Errorf("invalid json: %s", C.GoString(_paths))
+		return EINVAL
+	}
+	w.Warmup(ctx, paths, int(numthreads), background, isEvict, isCheck, resp)
+	res, err := json.Marshal(resp)
+	if err != nil {
+		logger.Fatalf("json: %s", err)
+	}
+
+	*p_buf = (*byte)(C.malloc(C.size_t(len(res))))
+	buf := unsafe.Slice(*p_buf, len(res))
+
+	return int32(copy(buf, res))
+}
+
 func main() {
 }
diff --git a/sdk/python/juicefs/juicefs/juicefs.py b/sdk/python/juicefs/juicefs/juicefs.py
index b6d6ccbd..0f207e63 100644
--- a/sdk/python/juicefs/juicefs/juicefs.py
+++ b/sdk/python/juicefs/juicefs/juicefs.py
@@ -341,12 +341,53 @@ class Client(object):
         """Remove an extended attribute from a file."""
         self.lib.jfs_removeXattr(c_int64(_tid()), c_int64(self.h), _bin(path), _bin(name))
 
-    def clone(self, src, dst):
+    def clone(self, src, dst, preserve=False):
         """Clone a file."""
-        self.lib.jfs_clone(c_int64(_tid()), c_int64(self.h), _bin(src), _bin(dst))
+        self.lib.jfs_clone(c_int64(_tid()), c_int64(self.h), _bin(src), _bin(dst), c_bool(preserve))
 
-    # def summary(self, path, depth=0, entries=1):
-    #     """Get the summary of a directory."""
+    def info(self, path, recursive=False, strict=False):
+        """Get the information of a file or a directory."""
+        buf = c_void_p()
+        n = self.lib.jfs_info(c_int64(_tid()), c_int64(self.h), _bin(path), byref(buf), c_bool(recursive), c_bool(strict))
+        data = string_at(buf, n)
+        res = json.loads(str(data, encoding='utf-8'))
+
+        self.lib.free(buf)
+        return res
+
+    def summary(self, path, depth=0, entries=1):
+        """Get the summary of a directory."""
+        buf = c_void_p()
+
+        n = self.lib.jfs_gettreesummary(_tid(), self.h, _bin(path), c_uint8(depth), c_uint32(entries), byref(buf))
+        data = string_at(buf, n)
+        res = json.loads(str(data, encoding='utf-8'))
+
+        def parseSummary(entry, removefields):
+            for f in removefields:
+                entry.pop(f, None)
+
+            if entry["Dirs"] == 0:
+                entry.pop("Children", None)
+            elif entry.get("Children") is not None:
+                for v in entry["Children"]:
+                    parseSummary(v, removefields)
+
+        parseSummary(res, ["Inode"])
+        self.lib.free(buf)
+        return res
+
+    def warmup(self, paths, numthreads=10, background=False, isEvict=False, isCheck=False):
+        """Warm up a file or a directory."""
+        if type(paths) is not list:
+            paths = [paths]
+
+        buf = c_void_p()
+
+        n = self.lib.jfs_warmup(c_int64(_tid()), c_int64(self.h), json.dumps(paths).encode(), c_int32(numthreads), c_bool(background), c_bool(isEvict), c_bool(isCheck), byref(buf))
+        res = json.loads(str(string_at(buf, n), encoding='utf-8'))
+        self.lib.free(buf)
+        return res
 
 
 class File(object):
@@ -604,7 +645,6 @@ class File(object):
         self.write(''.join(lines) if self.encoding else b''.join(lines))
         self.flush()
 
-
 def test():
     volume = os.getenv("JFS_VOLUME", "test")
     meta = os.getenv("JFS_META", "redis://localhost")


Test patch:
diff --git a/.github/scripts/pysdk/pysdk_test.py b/.github/scripts/pysdk/pysdk_test.py
index 0038b8bc..470028a0 100644
--- a/.github/scripts/pysdk/pysdk_test.py
+++ b/.github/scripts/pysdk/pysdk_test.py
@@ -23,7 +23,7 @@ def create_file(filename, content=b'content'):
 class FileTests(unittest.TestCase):
     def setUp(self):
         if not v.exists(TESTFN):
-           v.mkdir(TESTFN)
+            v.mkdir(TESTFN)
 
     def test_read(self):
         with v.open(TESTFILE, "w+b") as fobj:
@@ -74,7 +74,7 @@ class UtimeTests(unittest.TestCase):
 class MakedirTests(unittest.TestCase):
     def setUp(self):
         if v.exists(TESTFN):
-           v.rmr(TESTFN)
+            v.rmr(TESTFN)
         v.mkdir(TESTFN)
 
     def test_makedir(self):
@@ -97,14 +97,14 @@ class MakedirTests(unittest.TestCase):
         # may not have been created, so we look for the outermost directory
         # that exists.
         if v.exists(path):
-           v.rmr(path)
+            v.rmr(path)
 
 class ChownFileTests(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
         if not v.exists(TESTFN):
-           v.mkdir(TESTFN)
+            v.mkdir(TESTFN)
 
     def test_chown_uid_gid_arguments_must_be_index(self):
         stat = v.stat(TESTFN)
@@ -156,7 +156,6 @@ class LinkTests(unittest.TestCase):
     def test_link(self):
         self._test_link(self.file1, self.file2)
 
-@unittest.skip("Skipping SummaryTests")
 class SummaryTests(unittest.TestCase):
     # /test/dir1/file
     #      /dir2
@@ -171,27 +170,49 @@ class SummaryTests(unittest.TestCase):
 
     def test_summary(self):
         res = v.summary(TESTFILE, depth=258, entries=2)
-        self.assertTrue(res=={"Length":7, "Files":1, "Dirs":0, "Size":4096})
+        self.assertTrue(normalize(res)==normalize({"Path": "file", "Type": 2, "Files":1, "Dirs":0, "Size":4096}))
         res = v.summary(TESTFN)
-        self.assertTrue(res=={"Length":14, "Files":2, "Dirs":3, "Size":20480, "Entries": None})
+        self.assertTrue(normalize(res)==normalize({"Path": "test", "Type": 2, "Files":2, "Dirs":3, "Size":20480}))
         res = v.summary(TESTFN, depth=257, entries=1)
-        self.assertTrue(res=={"Length":14, "Files":2, "Dirs":3, "Size":20480, "Entries":
-            {"dir1":{"Length":7, "Files":1, "Dirs":1, "Size":8192, "Entries": None}}})
+        self.assertTrue(normalize(res)==normalize({"Path": "test", "Type": 2, "Files":2, "Dirs":3, "Size":20480, "Children":[
+            {"Path": "dir1", "Type": 2, "Files":1, "Dirs":1, "Size":8192},{'Path': '...', 'Type': 1, 'Size': 8192, 'Files': 1, 'Dirs': 1}]}))
         res = v.summary(TESTFN, depth=258, entries=1)
-        self.assertTrue(res=={"Length":14, "Files":2, "Dirs":3, "Size":20480, "Entries":
-            {"dir1":{"Length":7, "Files":1, "Dirs":1, "Size":8192, "Entries": {
-                "file": {"Length": 7, "Size": 4096, "Files": 1, "Dirs": 0}
-            }}}})
+        self.assertTrue(normalize(res)==normalize(
+                        {
+                            "Path": "test", "Type": 2, "Files":2, "Dirs":3, "Size":20480, "Children":
+                            [
+                                {"Path": "dir1", "Type": 2, "Files":1, "Dirs":1, "Size":8192, "Children": [
+                                    {"Path": "dir1/file", "Type": 1, "Size": 4096, "Files": 1, "Dirs": 0}
+                                ]
+                                 },{'Path': '...', 'Type': 1, 'Size': 8192, 'Files': 1, 'Dirs': 1}
+                            ]}
+                        ))
         res = v.summary(TESTFN, depth=259, entries=4)
-        self.assertTrue(res=={"Length":14, "Files":2, "Dirs":3, "Size":20480, "Entries":
-            {
-                "dir1":{"Length":7, "Files":1, "Dirs":1, "Size":8192, "Entries": {
-                    "file": {"Length": 7, "Size": 4096, "Files": 1, "Dirs": 0}
-                    }},
-                "dir2": {"Length": 0, "Size": 4096, "Files": 0, "Dirs": 1, "Entries": {}},
-                "file": {"Length": 7, "Size": 4096, "Files": 1, "Dirs": 0}
-            }})
-        
+        self.assertTrue(normalize(res)==normalize(
+                        {
+                            "Path": "test", "Type": 2, "Files":2, "Dirs":3, "Size":20480, "Children":
+                            [
+                                {
+                                    "Path": "dir1", "Type": 2, "Files":1, "Dirs":1, "Size":8192, "Children":
+                                    [{"Path": "dir1/file", "Type": 1, "Size": 4096, "Files": 1, "Dirs": 0}]
+                                },{
+                                'Path': 'file', 'Type': 1, 'Size': 4096, 'Files': 1, 'Dirs': 0
+                            },{
+                                'Path': 'dir2', 'Type': 2, 'Size': 4096, 'Files': 0, 'Dirs': 1
+                            }
+                            ]}
+                        ))
+
+def normalize(d):
+    if isinstance(d, dict):
+        if "Children" in d:
+            d["Children"].sort(key=lambda x: x["Path"])
+        return {k: normalize(v) for k, v in d.items()}
+    elif isinstance(d, list):
+        return sorted((normalize(x) for x in d), key=lambda x: x.get("Path", ""))
+    else:
+        return d
+
 class NonLocalSymlinkTests(unittest.TestCase):
     def setUp(self):
         r"""
@@ -210,9 +231,9 @@ class NonLocalSymlinkTests(unittest.TestCase):
         assert v.readlink(src) == '../some_dir'
 
 class ExtendedAttributeTests(unittest.TestCase):
-#    def tearDown(self):
-#        if v.exists(TESTFN + "_xattr"):
-#            v.rmr(TESTFN + '_xattr')
+    #    def tearDown(self):
+    #        if v.exists(TESTFN + "_xattr"):
+    #            v.rmr(TESTFN + '_xattr')
 
     def _check_xattrs_str(self, s, getxattr, setxattr, removexattr, listxattr, **kwargs):
         fn = TESTFN + '_xattr'
@@ -220,9 +241,9 @@ class ExtendedAttributeTests(unittest.TestCase):
             v.unlink(fn)
         create_file(fn)
 
-#        with self.assertRaises(OSError) as cm:
-#            v.getxattr(fn, s("user.test"), **kwargs)
-#        self.assertEqual(cm.exception.errno, errno.ENODATA)
+        #        with self.assertRaises(OSError) as cm:
+        #            v.getxattr(fn, s("user.test"), **kwargs)
+        #        self.assertEqual(cm.exception.errno, errno.ENODATA)
 
         init_xattr = v.listxattr(fn)
         self.assertIsInstance(init_xattr, list)
@@ -239,9 +260,9 @@ class ExtendedAttributeTests(unittest.TestCase):
             v.setxattr(fn, s("user.test"), b"bye", os.XATTR_CREATE, **kwargs)
         self.assertEqual(cm.exception.errno, errno.EEXIST)
 
-#        with self.assertRaises(OSError) as cm:
-#            v.setxattr(fn, s("user.test2"), b"bye", os.XATTR_REPLACE, **kwargs)
-#        self.assertEqual(cm.exception.errno, errno.ENODATA)
+        #        with self.assertRaises(OSError) as cm:
+        #            v.setxattr(fn, s("user.test2"), b"bye", os.XATTR_REPLACE, **kwargs)
+        #        self.assertEqual(cm.exception.errno, errno.ENODATA)
 
         v.setxattr(fn, s("user.test2"), b"foo", os.XATTR_CREATE, **kwargs)
         xattr.add("user.test2")
@@ -291,6 +312,7 @@ class ExtendedAttributeTests(unittest.TestCase):
                 return v.listxattr(fp.fileno(), *args)
         self._check_xattrs(getxattr, setxattr, removexattr, listxattr)
 
+
 class BenchTests(unittest.TestCase):
     def setUp(self):
         if not v.exists(TESTFN):
@@ -298,9 +320,9 @@ class BenchTests(unittest.TestCase):
         self.test_file = TESTFILE + '_bench'
         self.block_size = 128 * 1024  # 128KB
         self.buffer_size = 300
-        self.buffering = 2 * 1024 * 1024 
+        self.buffering = 2 * 1024 * 1024
         self.run_time = 30
-        self.file_size = 100 * 1024 * 1024  
+        self.file_size = 100 * 1024 * 1024
         self.seed = 20
         self.count = 200
 
@@ -343,7 +365,7 @@ class BenchTests(unittest.TestCase):
         print('test_seq_read')
         with v.open(self.test_file, 'wb') as f:
             f.write(os.urandom(self.file_size))
-        
+
         seq_read(
             filename=self.test_file,
             client=v,
@@ -356,7 +378,7 @@ class BenchTests(unittest.TestCase):
         print('test_random_read')
         with v.open(self.test_file, 'wb') as f:
             f.write(os.urandom(self.file_size))
-        
+
         random_read(
             filename=self.test_file,
             client=v,
diff --git a/pkg/fs/fs_test.go b/pkg/fs/fs_test.go
index d1d91cbe..e5cb037a 100644
--- a/pkg/fs/fs_test.go
+++ b/pkg/fs/fs_test.go
@@ -133,7 +133,7 @@ func TestFileSystem(t *testing.T) {
 	if e := f.Utime(ctx, 1, 2); e != 0 {
 		t.Fatalf("utime: %s", e)
 	}
-	if s, e := f.Summary(ctx); e != 0 || s.Dirs != 0 || s.Files != 1 || s.Length != 5 || s.Size != 4<<10 {
+	if s, e := f.Summary(ctx, true, true); e != 0 || s.Dirs != 0 || s.Files != 1 || s.Length != 5 || s.Size != 4<<10 {
 		t.Fatalf("summary: %s %+v", e, s)
 	}
 	if e := f.Close(ctx); e != 0 {
@@ -218,7 +218,7 @@ func TestFileSystem(t *testing.T) {
 		t.Fatalf("follow symlink: %s %+v", e, fi)
 	}
 
-	if s, e := d.Summary(ctx); e != 0 || s.Dirs != 2 || s.Files != 2 || s.Length != 7 || s.Size != 16<<10 {
+	if s, e := d.Summary(ctx, true, true); e != 0 || s.Dirs != 2 || s.Files != 2 || s.Length != 7 || s.Size != 16<<10 {
 		t.Fatalf("summary: %s %+v", e, s)
 	}
 	if e := fs.Delete(ctx, "/d"); e == 0 || !IsNotEmpty(e) {
diff --git a/pkg/object/object_storage_test.go b/pkg/object/object_storage_test.go
index e12a72de..b786ab3a 100644
--- a/pkg/object/object_storage_test.go
+++ b/pkg/object/object_storage_test.go
@@ -24,6 +24,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
 	"github.com/baidubce/bce-sdk-go/services/bos/api"
 	"github.com/huaweicloud/huaweicloud-sdk-go-obs/obs"
 	"io"
@@ -44,8 +45,6 @@ import (
 
 	"github.com/volcengine/ve-tos-golang-sdk/v2/tos/enum"
 
-	"github.com/aliyun/aliyun-oss-go-sdk/oss"
-
 	"github.com/redis/go-redis/v9"
 )
 
@@ -84,7 +83,7 @@ func setStorageClass(o ObjectStorage) string {
 		case *gs:
 			sc = "NEARLINE"
 		case *ossClient:
-			sc = string(oss.StorageIA)
+			sc = string(oss.StorageClassIA)
 		case *tosClient:
 			sc = string(enum.StorageClassIa)
 		case *obsClient:
diff --git a/pkg/vfs/fill_test.go b/pkg/vfs/fill_test.go
index a1f574e0..5359f531 100644
--- a/pkg/vfs/fill_test.go
+++ b/pkg/vfs/fill_test.go
@@ -36,7 +36,7 @@ func TestFill(t *testing.T) {
 	_, _ = v.Symlink(ctx, "testfile", 1, "sym3")
 
 	// normal cases
-	v.cache(meta.Background(), WarmupCache, []string{"/test/file", "/test", "/sym", "/"}, 2, nil)
+	v.cacheFiller.Cache(meta.Background(), WarmupCache, []string{"/test/file", "/test", "/sym", "/"}, 2, nil)
 
 	// remove chunk
 	var slices []meta.Slice
@@ -45,5 +45,5 @@ func TestFill(t *testing.T) {
 		_ = v.Store.Remove(s.Id, int(s.Size))
 	}
 	// bad cases
-	v.cache(meta.Background(), WarmupCache, []string{"/test/file", "/sym2", "/sym3", "/.stats", "/not_exists"}, 2, nil)
+	v.cacheFiller.Cache(meta.Background(), WarmupCache, []string{"/test/file", "/sym2", "/sym3", "/.stats", "/not_exists"}, 2, nil)
 }