Repository: juicedata/juicefs
Pull request #5857 (2025-03-25 10:07:29)

Fix release cache lock when calling `getDiskUsage`
`getDiskUsage` should not be called with cache lock held - this may cause high latency for other cache related operations.

In our scenario, we observed `cacheStore.load` experiences very high latency sometimes, even when the cache is still in memory. Additionally, metrics occasionally fail to scrape because `cacheStore.stats` also requires this lock.


Patch:
diff --git a/.github/scripts/command/dump_load.sh b/.github/scripts/command/dump_load.sh
index 524965a9..05b14e86 100755
--- a/.github/scripts/command/dump_load.sh
+++ b/.github/scripts/command/dump_load.sh
@@ -160,7 +160,7 @@ test_dump_load_with_random_test()
     prepare_test
     ./juicefs format $META_URL myjfs --enable-acl
     ./juicefs mount -d $META_URL /jfs 
-    ./random-test runOp -baseDir /jfs/test -files 500000 -ops 5000000 -threads 50 -dirSize 10 -duration 30s -createOp 30,uniform -deleteOp 5,end --linkOp 10,uniform --removeLinkOp 1,end --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform    
+    ./random-test runOp -baseDir /jfs/test -files 500000 -ops 5000000 -threads 50 -dirSize 100 -duration 30s -createOp 30,uniform -deleteOp 5,end --linkOp 10,uniform --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform    
     ./juicefs dump $META_URL dump.json $(get_dump_option)
     create_database $META_URL2
     ./juicefs load $META_URL2 dump.json $(get_load_option)
diff --git a/.github/scripts/command/fsck.sh b/.github/scripts/command/fsck.sh
index 7ad6f6dc..27ef8b65 100755
--- a/.github/scripts/command/fsck.sh
+++ b/.github/scripts/command/fsck.sh
@@ -54,11 +54,11 @@ test_fsck_with_random_test()
     prepare_test
     ./juicefs format $META_URL myjfs
     ./juicefs mount -d $META_URL /jfs
-    ./random-test runOp -baseDir /jfs/test -files 500000 -ops 5000000 -threads 50 -dirSize 10 -duration 30s -createOp 30,uniform -deleteOp 5,end --linkOp 10,uniform --removeLinkOp 1,end --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform    
+    ./random-test runOp -baseDir /jfs/test -files 500000 -ops 5000000 -threads 50 -dirSize 100 -duration 30s -createOp 30,uniform -deleteOp 5,end --linkOp 10,uniform  --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform    
     ./juicefs fsck $META_URL --path /test --sync-dir-stat --repair -r
     ./juicefs info -r /jfs | tee info1.log
     ./juicefs info -r /jfs --strict | tee info2.log
-    diff info1.log info2.log
+    diff info1.log info2.log || true
 }
 
 test_fsck_delete_object()
diff --git a/.github/scripts/sync/sync_cluster.sh b/.github/scripts/sync/sync_cluster.sh
index 415a8399..3b7c0a6e 100755
--- a/.github/scripts/sync/sync_cluster.sh
+++ b/.github/scripts/sync/sync_cluster.sh
@@ -185,8 +185,8 @@ test_sync_with_random_test(){
     mkdir /jfs/test || true
     mkdir /jfs/test2 || true
     
-    ./random-test runOp -baseDir /jfs/test -files 500000 -ops 5000000 -threads 50 -dirSize 10 -duration 60s -createOp 30,uniform \
-    -deleteOp 5,end --linkOp 10,uniform --removeLinkOp 1,end --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform
+    ./random-test runOp -baseDir /jfs/test -files 500000 -ops 5000000 -threads 50 -dirSize 100 -duration 60s -createOp 30,uniform \
+    -deleteOp 5,end --linkOp 10,uniform --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform
     chmod -R 777 /jfs/test
     chmod -R 777 /jfs/test2
     sudo -u juicedata meta_url=$META_URL ./juicefs sync jfs://meta_url/test/ jfs://meta_url/test2/ \
@@ -215,7 +215,7 @@ test_sync_files_from_file(){
     mkdir /jfs/test || true
     mkdir /jfs/test2 || true
     sudo -u juicedata ./random-test runOp -baseDir /jfs/test -files 500000 -ops 5000000 -threads 50 -dirSize 100000 -duration 60s -createOp 30,uniform \
-    -deleteOp 5,end --linkOp 10,uniform --removeLinkOp 1,end --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform
+    -deleteOp 5,end --linkOp 10,uniform --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform
     sudo ls /jfs/test > files
     sudo -u juicedata meta_url=$META_URL ./juicefs sync jfs://meta_url/test/ jfs://meta_url/test2/ \
          --manager-addr 172.20.0.1:8081 --worker juicedata@172.20.0.2,juicedata@172.20.0.3 \
diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml
index 5013133d..d8ea637f 100644
--- a/.github/workflows/compile.yml
+++ b/.github/workflows/compile.yml
@@ -93,7 +93,7 @@ jobs:
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
-          sudo ./juicefs format $meta_url --trash-days 0 pics
+          sudo ./juicefs format $meta_url --trash-days 0 --bucket=/mnt/jfs pics
 
       - name: Juicefs Mount
         run: |
diff --git a/.github/workflows/fsrand.yml b/.github/workflows/fsrand.yml
index e72a5b85..c96ebbba 100644
--- a/.github/workflows/fsrand.yml
+++ b/.github/workflows/fsrand.yml
@@ -86,7 +86,7 @@ jobs:
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
-          GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url --enable-acl --trash-days 0 myjfs
+          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url --enable-acl --trash-days 0 --bucket=/mnt/jfs myjfs
 
       - name: Juicefs Mount
         run: |
diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml
index d84d4070..838f6a65 100644
--- a/.github/workflows/gateway.yml
+++ b/.github/workflows/gateway.yml
@@ -111,7 +111,8 @@ jobs:
           volume="myjfs"
           export MINIO_ROOT_USER=minioadmin
           export MINIO_ROOT_PASSWORD=minioadmin
-          ./juicefs format $meta_url $volume --trash-days 0
+          sudo chmod 777 /mnt
+          ./juicefs format $meta_url $volume --trash-days 0 --bucket=/mnt/jfs
           ./juicefs gateway $meta_url localhost:8080 --no-usage-report --access-log /tmp/access1.log &
         
       - name: Sync with multiple process
diff --git a/.github/workflows/ltpfs.yml b/.github/workflows/ltpfs.yml
index 29e69a6e..991dd9ad 100644
--- a/.github/workflows/ltpfs.yml
+++ b/.github/workflows/ltpfs.yml
@@ -52,7 +52,7 @@ jobs:
 
       - name: Juicefs Format
         run: |
-          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format --trash-days 0 redis://127.0.0.1:6379/1 pics
+          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format --trash-days 0 redis://127.0.0.1:6379/1 --bucket=/mnt/jfs pics
 
       - name: Juicefs Mount
         run: |
diff --git a/.github/workflows/ltpsyscalls.yml b/.github/workflows/ltpsyscalls.yml
index 66606340..7d5e02b1 100644
--- a/.github/workflows/ltpsyscalls.yml
+++ b/.github/workflows/ltpsyscalls.yml
@@ -80,7 +80,7 @@ jobs:
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
-          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format --trash-days 0 $meta_url pics
+          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format --trash-days 0 $meta_url --bucket=/mnt/jfs pics
 
       - name: Juicefs Mount
         run: |
diff --git a/.github/workflows/pysdk.yml b/.github/workflows/pysdk.yml
index 940d2b54..3252ba77 100644
--- a/.github/workflows/pysdk.yml
+++ b/.github/workflows/pysdk.yml
@@ -11,7 +11,7 @@ on:
     - '**/hypo/fs_sdk_test.py'
     - '**/pysdk_test.py'
     - '**/juicefs.py'
-    - '**/pysdk.yml'
+#    - '**/pysdk.yml'
   pull_request:
     branches:
     - main
@@ -22,7 +22,7 @@ on:
     - '**/hypo/fs_sdk_test.py'
     - '**/pysdk_test.py'
     - '**/juicefs.py'
-    - '**/pysdk.yml'
+#    - '**/pysdk.yml'
   schedule:
     - cron:  '0 19 * * *'
   workflow_dispatch:
@@ -110,7 +110,7 @@ jobs:
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
-          sudo ./juicefs format $meta_url --trash-days 0 --enable-acl pics
+          sudo ./juicefs format $meta_url --trash-days 0 --enable-acl --bucket=/mnt/jfs pics
 
       - name: Juicefs Mount
         run: |
@@ -124,42 +124,49 @@ jobs:
         run: |
           make -C sdk/python/ libjfs.so
           sudo python3 sdk/python/juicefs/setup.py install
+          df -h
 
       - name: Run juicefs.py
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
           sudo JFS_VOLUME=test-volume JFS_META=$meta_url python3 sdk/python/juicefs/juicefs/juicefs.py
+          df -h
 
       - name: Run pysdk_test.py
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
           sudo META_URL=$meta_url python3 .github/scripts/pysdk/pysdk_test.py
+          df -h
 
       - name: Run file_test.py
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
           sudo USE_SDK=true META_URL=$meta_url python3 .github/scripts/hypo/file_test.py
+          df -h
 
       - name: Run file.py without read
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
           sudo USE_SDK=true META_URL=$meta_url EXCLUDE_RULES="read,readline,readlines" python3 .github/scripts/hypo/file.py
+          df -h
 
       - name: Run file.py without write
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
           sudo USE_SDK=true META_URL=$meta_url EXCLUDE_RULES="write,writelines,truncate" python3 .github/scripts/hypo/file.py
+          df -h
 
       - name: Run fs_sdk_test.py
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
           sudo USE_SDK=true META_URL=$meta_url python3 .github/scripts/hypo/fs_sdk_test.py
+          df -h
 
       - name: Run fs.py
         timeout-minutes: 60
@@ -168,6 +175,7 @@ jobs:
           meta_url=$(get_meta_url ${{matrix.meta}})
           sudo USE_SDK=true META_URL=$meta_url EXCLUDE_RULES="readlines,readline,clone_cp_file,clone_cp_dir" python3 .github/scripts/hypo/fs.py 2>&1 | tee fsrand.log
           exit ${PIPESTATUS[0]}
+          df -h
 
       - name: check fsrand.log
         if: always()
diff --git a/.github/workflows/rmfiles.yml b/.github/workflows/rmfiles.yml
index abf8353d..42b39521 100644
--- a/.github/workflows/rmfiles.yml
+++ b/.github/workflows/rmfiles.yml
@@ -73,7 +73,8 @@ jobs:
           # wget -q https://s.juicefs.com/static/bench/500K_empty_files.dump.gz
           # gzip -dk  500K_empty_files.dump.gz
           # ./juicefs load $meta_url 500K_empty_files.dump
-          GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url jfs
+          sudo chmod 777 /mnt
+          GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url --bucket=/mnt/jfs jfs
           GOCOVERDIR=$(pwd)/cover ./juicefs mdtest $meta_url test --dirs 10 --depth 3 --files 10 --threads 10 --no-usage-report
           GOCOVERDIR=$(pwd)/cover ./juicefs mount -d $meta_url $mp --no-usage-report
           sleep 3
@@ -106,7 +107,7 @@ jobs:
           # wget -q https://s.juicefs.com/static/bench/500K_empty_files.dump.gz
           # gzip -dk  500K_empty_files.dump.gz
           # ./juicefs load $meta_url 500K_empty_files.dump
-          GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url jfs
+          GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url --bucket=/mnt/jfs jfs
           GOCOVERDIR=$(pwd)/cover ./juicefs mdtest $meta_url test --dirs 10 --depth 3 --files 15 --threads 10 --no-usage-report
           GOCOVERDIR=$(pwd)/cover ./juicefs mount -d $meta_url $mp --no-usage-report
           sleep 3
diff --git a/.github/workflows/vdbench.yml b/.github/workflows/vdbench.yml
index b3f86391..08b29ded 100644
--- a/.github/workflows/vdbench.yml
+++ b/.github/workflows/vdbench.yml
@@ -94,7 +94,8 @@ jobs:
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
-          GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url vdbench-long-run --trash-days 0
+          sudo chmod 777 /mnt
+          GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url vdbench-long-run --trash-days 0 --bucket=/mnt/jfs
           GOCOVERDIR=$(pwd)/cover ./juicefs mount -d $meta_url /tmp/jfs --no-usage-report --cache-size 1024 --max-deletes 50
           vdbench/vdbench -f .github/workflows/resources/vdbench_long_run.conf -jn
 
diff --git a/.github/workflows/xattr.yml b/.github/workflows/xattr.yml
index e916d665..f71ead12 100644
--- a/.github/workflows/xattr.yml
+++ b/.github/workflows/xattr.yml
@@ -97,7 +97,7 @@ jobs:
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
-          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url --trash-days 0 pics
+          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url --trash-days 0 --bucket=/mnt/jfs pics
 
       - name: Juicefs Mount
         run: |
diff --git a/cmd/debug.go b/cmd/debug.go
index 152a2d69..89fc70c7 100644
--- a/cmd/debug.go
+++ b/cmd/debug.go
@@ -126,69 +126,6 @@ func copyFile(srcPath, destPath string, requireRootPrivileges bool) error {
 	return exec.CommandContext(ctx, copyArgs[0], copyArgs[1:]...).Run()
 }
 
-func getCmdMount(mp string) (uid, pid, cmd string, err error) {
-	var tmpPid string
-	_ = utils.WithTimeout(func() error {
-		content, err := readConfig(mp)
-		if err != nil {
-			logger.Warnf("failed to read config file: %v", err)
-		}
-		cfg := vfs.Config{}
-		if err := json.Unmarshal(content, &cfg); err != nil {
-			logger.Warnf("failed to unmarshal config file: %v", err)
-		}
-		if cfg.Pid != 0 {
-			tmpPid = strconv.Itoa(cfg.Pid)
-		}
-		return nil
-	}, 3*time.Second)
-
-	var psArgs []string
-	if tmpPid != "" {
-		pid = tmpPid
-		psArgs = []string{"/bin/sh", "-c", fmt.Sprintf("ps -f -p %s", pid)}
-	} else {
-		psArgs = []string{"/bin/sh", "-c", fmt.Sprintf("ps -ef | grep -v grep | grep mount | grep %s", mp)}
-	}
-	ret, err := exec.Command(psArgs[0], psArgs[1:]...).CombinedOutput()
-	if err != nil {
-		return "", "", "", fmt.Errorf("failed to execute command `%s`: %v", strings.Join(psArgs, " "), err)
-	}
-	var find bool
-	var ppid string
-	lines := strings.Split(string(ret), "\n")
-	for i := len(lines) - 1; i >= 0; i-- {
-		line := lines[i]
-		fields := strings.Fields(line)
-		if len(fields) <= 7 {
-			continue
-		}
-		cmdFields := fields[7:]
-		for _, arg := range cmdFields {
-			if mp == arg {
-				if find {
-					newCmd := strings.Join(fields[7:], " ")
-					newUid, newPid, newPpid := strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1]), strings.TrimSpace(fields[2])
-					if newPid == ppid {
-						return uid, pid, cmd, nil
-					} else if pid == newPpid {
-						return newUid, newPid, newCmd, nil
-					} else {
-						return "", "", "", fmt.Errorf("find more than one mount process for %s", mp)
-					}
-				}
-				cmd = strings.Join(fields[7:], " ")
-				uid, pid, ppid = strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1]), strings.TrimSpace(fields[2])
-				find = true
-			}
-		}
-	}
-	if cmd == "" {
-		return "", "", "", fmt.Errorf("no mount command found for %s", mp)
-	}
-	return uid, pid, cmd, nil
-}
-
 var logArg = regexp.MustCompile(`--log(\s*=?\s*)(\S+)`)
 
 func getLogPath(cmd string) (string, error) {
@@ -358,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" {
@@ -424,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)
@@ -477,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
 	}
@@ -493,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 {
@@ -587,6 +527,9 @@ func debug(ctx *cli.Context) error {
 	}
 	timestamp := time.Now().Format("20060102150405")
 	prefix := strings.Trim(strings.Join(strings.Split(amp, "/"), "-"), "-")
+	if runtime.GOOS == "windows" {
+		prefix = strings.ReplaceAll(prefix, ":", "")
+	}
 	outDir := ctx.String("out-dir")
 	currDir := filepath.Join(outDir, fmt.Sprintf("%s-%s", prefix, timestamp))
 	if err := os.MkdirAll(currDir, os.ModePerm); err != nil {
@@ -615,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_unix.go b/cmd/debug_unix.go
new file mode 100644
index 00000000..ea8b886d
--- /dev/null
+++ b/cmd/debug_unix.go
@@ -0,0 +1,95 @@
+//go:build !windows
+// +build !windows
+
+/*
+ * JuiceFS, Copyright 2025 Juicedata, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"encoding/json"
+	"fmt"
+	"os/exec"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/juicedata/juicefs/pkg/vfs"
+)
+
+func getCmdMount(mp string) (uid, pid, cmd string, err error) {
+	var tmpPid string
+	_ = utils.WithTimeout(func() error {
+		content, err := readConfig(mp)
+		if err != nil {
+			logger.Warnf("failed to read config file: %v", err)
+		}
+		cfg := vfs.Config{}
+		if err := json.Unmarshal(content, &cfg); err != nil {
+			logger.Warnf("failed to unmarshal config file: %v", err)
+		}
+		if cfg.Pid != 0 {
+			tmpPid = strconv.Itoa(cfg.Pid)
+		}
+		return nil
+	}, 3*time.Second)
+
+	var psArgs []string
+	if tmpPid != "" {
+		pid = tmpPid
+		psArgs = []string{"/bin/sh", "-c", fmt.Sprintf("ps -f -p %s", pid)}
+	} else {
+		psArgs = []string{"/bin/sh", "-c", fmt.Sprintf("ps -ef | grep -v grep | grep mount | grep %s", mp)}
+	}
+	ret, err := exec.Command(psArgs[0], psArgs[1:]...).CombinedOutput()
+	if err != nil {
+		return "", "", "", fmt.Errorf("failed to execute command `%s`: %v", strings.Join(psArgs, " "), err)
+	}
+	var find bool
+	var ppid string
+	lines := strings.Split(string(ret), "\n")
+	for i := len(lines) - 1; i >= 0; i-- {
+		line := lines[i]
+		fields := strings.Fields(line)
+		if len(fields) <= 7 {
+			continue
+		}
+		cmdFields := fields[7:]
+		for _, arg := range cmdFields {
+			if mp == arg {
+				if find {
+					newCmd := strings.Join(fields[7:], " ")
+					newUid, newPid, newPpid := strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1]), strings.TrimSpace(fields[2])
+					if newPid == ppid {
+						return uid, pid, cmd, nil
+					} else if pid == newPpid {
+						return newUid, newPid, newCmd, nil
+					} else {
+						return "", "", "", fmt.Errorf("find more than one mount process for %s", mp)
+					}
+				}
+				cmd = strings.Join(fields[7:], " ")
+				uid, pid, ppid = strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1]), strings.TrimSpace(fields[2])
+				find = true
+			}
+		}
+	}
+	if cmd == "" {
+		return "", "", "", fmt.Errorf("no mount command found for %s", mp)
+	}
+	return uid, pid, cmd, nil
+}
diff --git a/cmd/debug_windows.go b/cmd/debug_windows.go
new file mode 100644
index 00000000..a757c9ec
--- /dev/null
+++ b/cmd/debug_windows.go
@@ -0,0 +1,192 @@
+package cmd
+
+/*
+ * JuiceFS, Copyright 2025 Juicedata, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/juicedata/juicefs/pkg/vfs"
+	"golang.org/x/sys/windows"
+)
+
+func getprocessCommandLine(pid int) (string, error) {
+	cmd := exec.Command("wmic", "process", "where", "ProcessID="+strconv.Itoa(pid), "get", "CommandLine")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("failed to run command line: %s, %v", cmd.String(), err)
+	}
+
+	lines := strings.Split(string(out), "\r\n")
+	if len(lines) < 2 {
+		return "", fmt.Errorf("failed to find command line for pid: %d", pid)
+	}
+
+	for _, line := range lines[1:] {
+		sline := strings.TrimSpace(line)
+		if sline == "" {
+			continue
+		}
+		return sline, nil
+	}
+
+	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) {
+	processName := filepath.Base(os.Args[0])
+	cmd := exec.Command("wmic", "process", "where", fmt.Sprintf("name='%s'", processName), "get", "CommandLine,ProcessId")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return 0, fmt.Errorf("failed to exec command line: %s, %s", cmd.String(), err)
+	}
+
+	lines := strings.Split(string(out), "\r\n")
+	if len(lines) < 2 {
+		return 0, fmt.Errorf("failed to find mount process")
+	}
+
+	mp = strings.TrimRight(mp, "\\")
+	for _, line := range lines[1:] {
+		sline := strings.TrimSpace(line)
+
+		if sline == "" {
+			continue
+		}
+
+		// the first part of commandline contains 'xxx/mount.exe"'
+		slines := strings.SplitN(sline, ".exe\" ", 2)
+		if len(slines) < 2 {
+			logger.Warnf("failed to split command line: %s", sline)
+			continue
+		}
+
+		sline = slines[1]
+		logger.Infof("sline: %s", sline)
+
+		args := strings.Split(sline, " ")
+		if len(args) < 3 {
+			continue
+		}
+		mpFound := false
+		mountFound := false
+		for _, arg := range args {
+			arg = strings.TrimSpace(arg)
+			if arg == "" {
+				continue
+			}
+
+			if arg == "mount" {
+				mountFound = true
+				continue
+			}
+
+			arg = strings.TrimRight(arg, "\\")
+
+			if strings.EqualFold(arg, mp) {
+				mpFound = true
+			}
+		}
+
+		if mpFound && mountFound {
+			// THE LAST PART IS PID
+			pid, err := strconv.Atoi(args[len(args)-1])
+			if err != nil {
+				return 0, fmt.Errorf("failed to parse pid: %s", args[len(args)-1])
+			}
+			return pid, nil
+		}
+	}
+
+	return 0, fmt.Errorf("cannot find the mount process for %s", mp)
+}
+
+func getProcessUserSid(pid int) (string, error) {
+	h, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid))
+	if err != nil {
+		return "", err
+	}
+	defer windows.CloseHandle(h)
+
+	var token windows.Token
+	err = windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token)
+	if err != nil {
+		return "", err
+	}
+	defer token.Close()
+
+	user, err := token.GetTokenUser()
+	if err != nil {
+		return "", err
+	}
+
+	return user.User.Sid.String(), nil
+
+}
+
+func getCmdMount(mp string) (uid, pid, cmd string, err error) {
+	var tmpPid string
+	_ = utils.WithTimeout(func() error {
+		content, err := readConfig(mp)
+		if err != nil {
+			logger.Warnf("failed to read config file: %v", err)
+		}
+		cfg := vfs.Config{}
+		if err := json.Unmarshal(content, &cfg); err != nil {
+			logger.Warnf("failed to unmarshal config file: %v", err)
+		}
+		if cfg.Pid != 0 {
+			tmpPid = strconv.Itoa(cfg.Pid)
+		}
+		return nil
+	}, 3*time.Second)
+
+	foundPid := 0
+	if tmpPid != "" {
+		pid = tmpPid
+		foundPid, err = strconv.Atoi(pid)
+		if err != nil {
+			return "", "", "", fmt.Errorf("failed to parse pid: %s", pid)
+		}
+	} else {
+		foundPid, err = findMountProcess(mp)
+		if err != nil {
+			return "", "", "", err
+		}
+
+		pid = strconv.Itoa(foundPid)
+	}
+
+	cmd, err = getprocessCommandLine(foundPid)
+	if err != nil {
+		return "", "", "", err
+	}
+
+	uid, err = getProcessUserSid(foundPid)
+	if err != nil {
+		return "", "", "", err
+	}
+
+	return uid, pid, cmd, nil
+}
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/cmd/status.go b/cmd/status.go
index c00409fa..efb62e70 100644
--- a/cmd/status.go
+++ b/cmd/status.go
@@ -17,11 +17,13 @@
 package cmd
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
-	"syscall"
-	"time"
+	"os"
 
+	"github.com/juicedata/juicefs/pkg/chunk"
+	"github.com/juicedata/juicefs/pkg/fs"
 	"github.com/juicedata/juicefs/pkg/meta"
 	"github.com/juicedata/juicefs/pkg/utils"
 
@@ -58,107 +60,58 @@ $ juicefs status redis://localhost`,
 	}
 }
 
-type sections struct {
-	Setting   *meta.Format
-	Sessions  []*meta.Session
-	Statistic *statistic
-}
-
-type statistic struct {
-	UsedSpace                uint64
-	AvailableSpace           uint64
-	UsedInodes               uint64
-	AvailableInodes          uint64
-	TrashFileCount           int64 `json:",omitempty"`
-	TrashFileSize            int64 `json:",omitempty"`
-	PendingDeletedFileCount  int64 `json:",omitempty"`
-	PendingDeletedFileSize   int64 `json:",omitempty"`
-	TrashSliceCount          int64 `json:",omitempty"`
-	TrashSliceSize           int64 `json:",omitempty"`
-	PendingDeletedSliceCount int64 `json:",omitempty"`
-	PendingDeletedSliceSize  int64 `json:",omitempty"`
-}
-
-func printJson(v interface{}) {
-	output, err := json.MarshalIndent(v, "", "  ")
-	if err != nil {
-		logger.Fatalf("json: %s", err)
+func printJson(data []byte) {
+	var out bytes.Buffer
+	if err := json.Indent(&out, data, "", "  "); err != nil {
+		logger.Fatalf("format JSON: %s", err)
 	}
-	fmt.Println(string(output))
+	formatted := out.Bytes()
+	fmt.Println(string(formatted))
 }
 
-func status(ctx *cli.Context) error {
-	setup(ctx, 1)
-	removePassword(ctx.Args().Get(0))
-	m := meta.NewClient(ctx.Args().Get(0), nil)
+func initForStatus(c *cli.Context, metaUrl string) *fs.FileSystem {
+	metaConf := getMetaConf(c, "status", false)
+	m := meta.NewClient(metaUrl, metaConf)
 	format, err := m.Load(true)
 	if err != nil {
 		logger.Fatalf("load setting: %s", err)
 	}
-	format.RemoveSecret()
-
-	if sid := ctx.Uint64("session"); sid != 0 {
-		s, err := m.GetSession(sid, true)
-		if err != nil {
-			logger.Fatalf("get session: %s", err)
-		}
-		printJson(s)
-		return nil
+	if st := m.Chroot(meta.Background(), metaConf.Subdir); st != 0 {
+		logger.Fatalf("Chroot to %s: %s", metaConf.Subdir, st)
 	}
 
-	sessions, err := m.ListSessions()
+	blob, err := NewReloadableStorage(format, m, updateFormat(c))
 	if err != nil {
-		logger.Fatalf("list sessions: %s", err)
+		logger.Fatalf("object storage: %s", err)
 	}
 
-	stat := &statistic{}
-	var totalSpace uint64
-	if err = m.StatFS(meta.Background(), meta.RootInode, &totalSpace, &stat.AvailableSpace, &stat.UsedInodes, &stat.AvailableInodes); err != syscall.Errno(0) {
-		logger.Fatalf("stat fs: %s", err)
+	chunkConf := getChunkConf(c, format)
+	store := chunk.NewCachedStore(blob, *chunkConf, nil)
+	conf := getVfsConf(c, metaConf, format, chunkConf)
+	conf.AccessLog = c.String("access-log")
+	conf.AttrTimeout = utils.Duration(c.String("attr-cache"))
+	conf.EntryTimeout = utils.Duration(c.String("entry-cache"))
+	conf.DirEntryTimeout = utils.Duration(c.String("dir-entry-cache"))
+	jfs, err := fs.NewFileSystem(conf, m, store)
+	if err != nil {
+		logger.Fatalf("initialize failed: %s", err)
 	}
-	stat.UsedSpace = totalSpace - stat.AvailableSpace
+	return jfs
+}
 
-	if ctx.Bool("more") {
-		progress := utils.NewProgress(false)
-		trashFileSpinner := progress.AddDoubleSpinner("Trash Files")
-		pendingDeletedFileSpinner := progress.AddDoubleSpinner("Pending Deleted Files")
-		trashSlicesSpinner := progress.AddDoubleSpinner("Trash Slices")
-		pendingDeletedSlicesSpinner := progress.AddDoubleSpinner("Pending Deleted Slices")
-		err = m.ScanDeletedObject(
-			meta.WrapContext(ctx.Context),
-			func(ss []meta.Slice, _ int64) (bool, error) {
-				for _, s := range ss {
-					trashSlicesSpinner.IncrInt64(int64(s.Size))
-				}
-				return false, nil
-			},
-			func(_ uint64, size uint32) (bool, error) {
-				pendingDeletedSlicesSpinner.IncrInt64(int64(size))
-				return false, nil
-			},
-			func(_ meta.Ino, size uint64, _ time.Time) (bool, error) {
-				trashFileSpinner.IncrInt64(int64(size))
-				return false, nil
-			},
-			func(_ meta.Ino, size uint64, _ int64) (bool, error) {
-				pendingDeletedFileSpinner.IncrInt64(int64(size))
-				return false, nil
-			},
-		)
-		if err != nil {
-			logger.Fatalf("statistic: %s", err)
-		}
-		trashSlicesSpinner.Done()
-		pendingDeletedSlicesSpinner.Done()
-		trashFileSpinner.Done()
-		pendingDeletedFileSpinner.Done()
-		progress.Done()
-		stat.TrashSliceCount, stat.TrashSliceSize = trashSlicesSpinner.Current()
-		stat.PendingDeletedSliceCount, stat.PendingDeletedSliceSize = pendingDeletedSlicesSpinner.Current()
-		stat.TrashFileCount, stat.TrashFileSize = trashFileSpinner.Current()
-		stat.PendingDeletedFileCount, stat.PendingDeletedFileSize = pendingDeletedFileSpinner.Current()
+func status(ctx *cli.Context) error {
+	setup(ctx, 1)
+	metaUrl := ctx.Args().Get(0)
+	removePassword(metaUrl)
+
+	jfs := initForStatus(ctx, metaUrl)
+	sessionId := ctx.Uint64("session")
+	context := meta.NewContext(1, uint32(os.Getuid()), []uint32{uint32(os.Getgid())})
+	output, err := jfs.Status(context, ctx.Bool("more"), sessionId)
+	if err != nil {
+		logger.Fatalf("status: %s", err)
 	}
 
-	printJson(&sections{format, sessions, stat})
+	printJson(output)
 	return nil
 }
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/chunk/cached_store.go b/pkg/chunk/cached_store.go
index 7ba9882b..ac47029e 100644
--- a/pkg/chunk/cached_store.go
+++ b/pkg/chunk/cached_store.go
@@ -1128,11 +1128,11 @@ func (store *cachedStore) FillCache(id uint64, length uint32) error {
 			continue
 		}
 		p := NewOffPage(size)
-		defer p.Release()
 		if e := store.load(k, p, true, true); e != nil {
 			logger.Warnf("Failed to load key: %s %s", k, e)
 			err = e
 		}
+		p.Release()
 	}
 	return err
 }
diff --git a/pkg/chunk/disk_cache.go b/pkg/chunk/disk_cache.go
index 3186f234..21d11678 100644
--- a/pkg/chunk/disk_cache.go
+++ b/pkg/chunk/disk_cache.go
@@ -140,12 +140,12 @@ func newCacheStore(m *cacheManagerMetrics, dir string, cacheSize, maxItems int64
 	}
 
 	c.createDir(c.dir)
-	br, fr := c.curFreeRatio()
-	if br < c.freeRatio || fr < c.freeRatio {
-		logger.Warnf("not enough space (%d%%) or inodes (%d%%) for caching in %s: free ratio should be >= %d%%", int(br*100), int(fr*100), c.dir, int(c.freeRatio*100))
+	usage := c.curFreeRatio()
+	if usage.br < c.freeRatio || usage.fr < c.freeRatio {
+		logger.Warnf("not enough space (%d%%) or inodes (%d%%) for caching in %s: free ratio should be >= %d%%", int(usage.br*100), int(usage.fr*100), c.dir, int(c.freeRatio*100))
 	}
 	logger.Infof("Disk cache (%s): used ratio - [space %s%%, inode %s%%]",
-		c.dir, humanize.FtoaWithDigits(float64((1-br)*100), 1), humanize.FtoaWithDigits(float64((1-fr)*100), 1))
+		c.dir, humanize.FtoaWithDigits(float64((1-usage.br)*100), 1), humanize.FtoaWithDigits(float64((1-usage.fr)*100), 1))
 	c.createLockFile()
 	go c.checkLockFile()
 	go c.flush()
@@ -258,7 +258,7 @@ func (c *cacheStore) checkTimeout() {
 		c.opMu.Lock()
 		for ts := range c.opTs {
 			if ts < cutOff {
-				logger.Errorf("IO operation %s on %s is timeout after %s, ", getFunctionName(c.opTs[ts]), c.dir, now-ts)
+				logger.Warnf("IO operation %s on %s is timeout after %s, ", getFunctionName(c.opTs[ts]), c.dir, now-ts)
 				c.state.onIOErr()
 				delete(c.opTs, ts)
 			}
@@ -312,16 +312,16 @@ func (cache *cacheStore) stats() (int64, int64) {
 
 func (cache *cacheStore) checkFreeSpace() {
 	for cache.available() {
-		br, fr := cache.curFreeRatio()
-		cache.stageFull = br < cache.freeRatio/2 || fr < cache.freeRatio/2
-		cache.rawFull = br < cache.freeRatio || fr < cache.freeRatio
+		usage := cache.curFreeRatio()
+		cache.stageFull = usage.br < cache.freeRatio/2 || usage.fr < cache.freeRatio/2
+		cache.rawFull = usage.br < cache.freeRatio || usage.fr < cache.freeRatio
 		if cache.rawFull && cache.eviction != "none" {
-			logger.Tracef("Cleanup cache when check free space (%s): free ratio (%d%%), space usage (%d%%), inodes usage (%d%%)", cache.dir, int(cache.freeRatio*100), int(br*100), int(fr*100))
+			logger.Tracef("Cleanup cache when check free space (%s): free ratio (%d%%), space usage (%d%%), inodes usage (%d%%)", cache.dir, int(cache.freeRatio*100), int(usage.br*100), int(usage.fr*100))
 			cache.Lock()
 			cache.cleanupFull()
 			cache.Unlock()
-			br, fr = cache.curFreeRatio()
-			cache.rawFull = br < cache.freeRatio || fr < cache.freeRatio
+			usage = cache.curFreeRatio()
+			cache.rawFull = usage.br < cache.freeRatio || usage.fr < cache.freeRatio
 		}
 		if cache.rawFull {
 			cache.uploadStaging()
@@ -439,13 +439,31 @@ func (cache *cacheStore) cache(key string, p *Page, force, dropCache bool) {
 	}
 }
 
-func (cache *cacheStore) curFreeRatio() (float32, float32) {
+type DiskFreeRatio struct {
+	br       float32
+	fr       float32
+	spaceCap uint64
+	inodeCap uint64
+}
+
+// caller should not hold cache lock
+func (cache *cacheStore) curFreeRatio() DiskFreeRatio {
 	var total, free, files, ffree uint64
 	_ = cache.checkErr(func() error {
 		total, free, files, ffree = getDiskUsage(cache.dir)
 		return nil
 	})
-	return float32(free) / float32(total), float32(ffree) / float32(files)
+	usage := DiskFreeRatio{
+		spaceCap: total,
+		inodeCap: files,
+	}
+	if total != 0 {
+		usage.br = float32(free) / float32(total)
+	}
+	if files != 0 {
+		usage.fr = float32(ffree) / float32(files)
+	}
+	return usage
 }
 
 func (cache *cacheStore) flushPage(path string, data []byte, dropCache bool) (err error) {
@@ -468,7 +486,7 @@ func (cache *cacheStore) flushPage(path string, data []byte, dropCache bool) (er
 		return err
 	})
 	if err != nil {
-		logger.Infof("Can't create cache file %s: %s", tmp, err)
+		logger.Warnf("Can't create cache file %s: %s", tmp, err)
 		return err
 	}
 
@@ -757,26 +775,29 @@ func (cache *cacheStore) cleanupFull() {
 	if cache.maxItems != 0 && num > cache.maxItems*99/100 {
 		num = cache.maxItems * 99 / 100
 	}
+	cache.Unlock()
 	// make sure we have enough free space after cleanup
-	br, fr := cache.curFreeRatio()
-	if br < cache.freeRatio {
-		total, _, _, _ := getDiskUsage(cache.dir)
-		toFree := int64(float32(total) * (cache.freeRatio - br))
+	usage := cache.curFreeRatio()
+	cache.Lock()
+	if usage.br < cache.freeRatio {
+		toFree := int64(float32(usage.spaceCap) * (cache.freeRatio - usage.br))
 		if toFree > cache.used {
 			goal = 0
 		} else if cache.used-toFree < goal {
 			goal = (cache.used - toFree) * 95 / 100
 		}
 	}
-	if fr < cache.freeRatio {
-		_, _, files, _ := getDiskUsage(cache.dir)
-		toFree := int(float32(files) * (cache.freeRatio - fr))
+	if usage.fr < cache.freeRatio {
+		toFree := int(float32(usage.inodeCap) * (cache.freeRatio - usage.fr))
 		if toFree > len(cache.keys) {
 			num = 0
 		} else {
 			num = int64(len(cache.keys)-toFree) * 99 / 100
 		}
 	}
+	if int64(len(cache.keys)) <= num && cache.used <= goal {
+		return // some other thread has done the cleanup
+	}
 
 	var todel []cacheKey
 	var freed int64
@@ -826,18 +847,16 @@ func (cache *cacheStore) cleanupFull() {
 }
 
 func (cache *cacheStore) uploadStaging() {
-	cache.Lock()
-	defer cache.Unlock()
 	if !cache.scanned || cache.uploader == nil {
 		return
 	}
-
 	var toFree int64
-	br, fr := cache.curFreeRatio()
-	if br < cache.freeRatio || fr < cache.freeRatio {
-		total, _, _, _ := getDiskUsage(cache.dir)
-		toFree = int64(float64(total)*float64(cache.freeRatio) - math.Min(float64(br), float64(fr)))
+	usage := cache.curFreeRatio()
+	if usage.br < cache.freeRatio || usage.fr < cache.freeRatio {
+		toFree = int64(float64(usage.spaceCap)*float64(cache.freeRatio) - math.Min(float64(usage.br), float64(usage.fr)))
 	}
+	cache.Lock()
+	defer cache.Unlock()
 	var cnt int
 	var lastK cacheKey
 	var lastValue cacheItem
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index 1174cf89..420ddf60 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -19,6 +19,7 @@ package fs
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"io"
 	"os"
@@ -129,10 +130,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 +168,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),
@@ -480,10 +485,21 @@ func (fs *FileSystem) MkdirAll0(ctx meta.Context, p string, mode uint16, umask u
 	return err
 }
 
+func (fs *FileSystem) Unlink(ctx meta.Context, p string) (err syscall.Errno) {
+	defer trace.StartRegion(context.TODO(), "fs.Unlink").End()
+	l := vfs.NewLogContext(ctx)
+	defer func() { fs.log(l, "Unlink (%s): %s", p, errstr(err)) }()
+	return fs.Delete0(ctx, p, true)
+}
+
 func (fs *FileSystem) Delete(ctx meta.Context, p string) (err syscall.Errno) {
 	defer trace.StartRegion(context.TODO(), "fs.Delete").End()
 	l := vfs.NewLogContext(ctx)
 	defer func() { fs.log(l, "Delete (%s): %s", p, errstr(err)) }()
+	return fs.Delete0(ctx, p, false)
+}
+
+func (fs *FileSystem) Delete0(ctx meta.Context, p string, callByUnlink bool) (err syscall.Errno) {
 	parent, err := fs.resolve(ctx, parentDir(p), true)
 	if err != 0 {
 		return
@@ -493,6 +509,10 @@ func (fs *FileSystem) Delete(ctx meta.Context, p string) (err syscall.Errno) {
 		return
 	}
 	if fi.IsDir() {
+		if callByUnlink {
+			err = syscall.EISDIR
+			return
+		}
 		err = fs.m.Rmdir(ctx, parent.inode, path.Base(p))
 	} else {
 		err = fs.m.Unlink(ctx, parent.inode, path.Base(p))
@@ -501,6 +521,19 @@ func (fs *FileSystem) Delete(ctx meta.Context, p string) (err syscall.Errno) {
 	return
 }
 
+func (fs *FileSystem) Rmdir(ctx meta.Context, p string) (err syscall.Errno) {
+	defer trace.StartRegion(context.TODO(), "fs.Rmdir").End()
+	l := vfs.NewLogContext(ctx)
+	defer func() { fs.log(l, "Rmdir (%s): %s", p, errstr(err)) }()
+	parent, err := fs.resolve(ctx, parentDir(p), true)
+	if err != 0 {
+		return
+	}
+	err = fs.m.Rmdir(ctx, parent.inode, path.Base(p))
+	fs.invalidateEntry(parent.inode, path.Base(p))
+	return
+}
+
 func (fs *FileSystem) Rmr(ctx meta.Context, p string, numthreads int) (err syscall.Errno) {
 	defer trace.StartRegion(context.TODO(), "fs.Rmr").End()
 	l := vfs.NewLogContext(ctx)
@@ -628,6 +661,9 @@ func (fs *FileSystem) Truncate(ctx meta.Context, path string, length uint64) (er
 	if err != 0 {
 		return
 	}
+	if fi.IsDir() {
+		return syscall.EISDIR
+	}
 	err = fs.m.Truncate(ctx, fi.inode, 0, length, nil, false)
 	return
 }
@@ -956,6 +992,157 @@ func (fs *FileSystem) Close() error {
 	return nil
 }
 
+type statistic struct {
+	UsedSpace                uint64
+	AvailableSpace           uint64
+	UsedInodes               uint64
+	AvailableInodes          uint64
+	TrashFileCount           int64 `json:",omitempty"`
+	TrashFileSize            int64 `json:",omitempty"`
+	PendingDeletedFileCount  int64 `json:",omitempty"`
+	PendingDeletedFileSize   int64 `json:",omitempty"`
+	TrashSliceCount          int64 `json:",omitempty"`
+	TrashSliceSize           int64 `json:",omitempty"`
+	PendingDeletedSliceCount int64 `json:",omitempty"`
+	PendingDeletedSliceSize  int64 `json:",omitempty"`
+}
+
+type sections struct {
+	Setting   *meta.Format
+	Sessions  []*meta.Session
+	Statistic *statistic
+}
+
+func (fs *FileSystem) Status(ctx meta.Context, trash bool, sessionId uint64) ([]byte, error) {
+	var totalSpace uint64
+	var err error
+
+	defer trace.StartRegion(context.TODO(), "fs.Status").End()
+	l := vfs.NewLogContext(ctx)
+	defer func() {
+		fs.log(l, "Status (%t): %s", trash, errstr(err))
+	}()
+
+	format, err := fs.m.Load(true)
+	if err != nil {
+		return nil, fmt.Errorf("load setting: %v", err)
+	}
+	format.RemoveSecret()
+
+	if sessionId != 0 {
+		s, err := fs.m.GetSession(sessionId, true)
+		if err != nil {
+			return nil, fmt.Errorf("get session: %v", err)
+		}
+		output, err := json.Marshal(s)
+		return output, err
+	}
+
+	sessions, err := fs.m.ListSessions()
+	if err != nil {
+		return nil, fmt.Errorf("list sessions: %v", err)
+	}
+
+	stat := &statistic{}
+	if err = fs.m.StatFS(meta.Background(), meta.RootInode, &totalSpace, &stat.AvailableSpace, &stat.UsedInodes, &stat.AvailableInodes); err != syscall.Errno(0) {
+		return nil, fmt.Errorf("stat fs: %v", err)
+	}
+	stat.UsedSpace = totalSpace - stat.AvailableSpace
+
+	if trash {
+		progress := utils.NewProgress(false)
+		trashFileSpinner := progress.AddDoubleSpinner("Trash Files")
+		pendingDeletedFileSpinner := progress.AddDoubleSpinner("Pending Deleted Files")
+		trashSlicesSpinner := progress.AddDoubleSpinner("Trash Slices")
+		pendingDeletedSlicesSpinner := progress.AddDoubleSpinner("Pending Deleted Slices")
+		err = fs.m.ScanDeletedObject(
+			meta.WrapContext(ctx),
+			func(ss []meta.Slice, _ int64) (bool, error) {
+				for _, s := range ss {
+					trashSlicesSpinner.IncrInt64(int64(s.Size))
+				}
+				return false, nil
+			},
+			func(_ uint64, size uint32) (bool, error) {
+				pendingDeletedSlicesSpinner.IncrInt64(int64(size))
+				return false, nil
+			},
+			func(_ meta.Ino, size uint64, _ time.Time) (bool, error) {
+				trashFileSpinner.IncrInt64(int64(size))
+				return false, nil
+			},
+			func(_ meta.Ino, size uint64, _ int64) (bool, error) {
+				pendingDeletedFileSpinner.IncrInt64(int64(size))
+				return false, nil
+			},
+		)
+		if err != nil {
+			return nil, fmt.Errorf("statistic: %v", err)
+		}
+
+		trashSlicesSpinner.Done()
+		pendingDeletedSlicesSpinner.Done()
+		trashFileSpinner.Done()
+		pendingDeletedFileSpinner.Done()
+		progress.Done()
+		stat.TrashSliceCount, stat.TrashSliceSize = trashSlicesSpinner.Current()
+		stat.PendingDeletedSliceCount, stat.PendingDeletedSliceSize = pendingDeletedSlicesSpinner.Current()
+		stat.TrashFileCount, stat.TrashFileSize = trashFileSpinner.Current()
+		stat.PendingDeletedFileCount, stat.PendingDeletedFileSize = pendingDeletedFileSpinner.Current()
+	}
+
+	output, err := json.Marshal(&sections{format, sessions, stat})
+	if err != nil {
+		return nil, fmt.Errorf("json: %v", err)
+	}
+	return output, 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 {
@@ -1295,13 +1482,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/meta/base.go b/pkg/meta/base.go
index cd539f6e..3459e520 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -3089,12 +3089,24 @@ func (m *baseMeta) NewDirHandler(ctx Context, inode Ino, plus bool, initEntries
 	initEntries = append(initEntries, &Entry{
 		Inode: inode,
 		Name:  []byte("."),
-		Attr:  &Attr{Typ: TypeDirectory},
-	}, &Entry{
+		Attr:  &attr,
+	})
+
+	parent := &Entry{
 		Inode: attr.Parent,
 		Name:  []byte(".."),
 		Attr:  &Attr{Typ: TypeDirectory},
-	})
+	}
+	if plus {
+		if attr.Parent == inode {
+			parent.Attr = &attr
+		} else {
+			if st := m.GetAttr(ctx, attr.Parent, parent.Attr); st != 0 {
+				return nil, st
+			}
+		}
+	}
+	initEntries = append(initEntries, parent)
 
 	return m.en.newDirHandler(inode, plus, initEntries), 0
 }
diff --git a/pkg/object/bos.go b/pkg/object/bos.go
index 7755cc46..84b31d0e 100644
--- a/pkg/object/bos.go
+++ b/pkg/object/bos.go
@@ -22,11 +22,13 @@ package object
 import (
 	"bytes"
 	"fmt"
+	"hash/crc32"
 	"io"
 	"net/http"
 	"net/url"
 	"os"
 	"sort"
+	"strconv"
 	"strings"
 	"time"
 
@@ -93,22 +95,32 @@ func (q *bosclient) Head(key string) (Object, error) {
 	}, nil
 }
 
-func (q *bosclient) Get(key string, off, limit int64, getters ...AttrGetter) (io.ReadCloser, error) {
+func (q *bosclient) Get(key string, off, limit int64, getters ...AttrGetter) (resp io.ReadCloser, err error) {
 	var r *api.GetObjectResult
-	var err error
+	var needCheck bool
 	if limit > 0 {
 		r, err = q.c.GetObject(q.bucket, key, nil, off, off+limit-1)
 	} else if off > 0 {
 		r, err = q.c.GetObject(q.bucket, key, nil, off)
 	} else {
 		r, err = q.c.GetObject(q.bucket, key, nil)
+		needCheck = true
 	}
 	if err != nil {
-		return nil, err
+		return
+	}
+	if needCheck {
+		if r.UserMeta[checksumAlgr] != "" {
+			resp = verifyChecksum(r.Body, r.UserMeta[checksumAlgr], r.ContentLength)
+		} else {
+			resp = verifyChecksum0(r.Body, r.ContentCrc32, r.ContentLength, crc32.IEEETable)
+		}
+	} else {
+		resp = r.Body
 	}
 	attrs := applyGetters(getters...)
 	attrs.SetStorageClass(r.StorageClass)
-	return r.Body, nil
+	return
 }
 
 func (q *bosclient) Put(key string, in io.Reader, getters ...AttrGetter) error {
@@ -135,6 +147,8 @@ func (q *bosclient) Put(key string, in io.Reader, getters ...AttrGetter) error {
 	if q.sc != "" {
 		args.StorageClass = q.sc
 	}
+	args.UserMeta = make(map[string]string)
+	args.UserMeta[checksumAlgr] = strconv.Itoa(int(crc32.Update(0, crc32c, data)))
 	_, err = q.c.PutObject(q.bucket, key, body, args)
 	attrs := applyGetters(getters...)
 	attrs.SetStorageClass(q.sc)
diff --git a/pkg/object/checksum.go b/pkg/object/checksum.go
index c7db5a38..6a69d7f2 100644
--- a/pkg/object/checksum.go
+++ b/pkg/object/checksum.go
@@ -57,19 +57,22 @@ type checksumReader struct {
 	expected        uint32
 	checksum        uint32
 	remainingLength int64
+	table           *crc32.Table
 }
 
 func (c *checksumReader) Read(buf []byte) (n int, err error) {
 	n, err = c.ReadCloser.Read(buf)
-	c.checksum = crc32.Update(c.checksum, crc32c, buf[:n])
+	c.checksum = crc32.Update(c.checksum, c.table, buf[:n])
 	c.remainingLength -= int64(n)
 	if (err == io.EOF || c.remainingLength == 0) && c.checksum != c.expected {
 		return 0, fmt.Errorf("verify checksum failed: %d != %d", c.checksum, c.expected)
 	}
 	return
 }
-
 func verifyChecksum(in io.ReadCloser, checksum string, contentLength int64) io.ReadCloser {
+	return verifyChecksum0(in, checksum, contentLength, crc32c)
+}
+func verifyChecksum0(in io.ReadCloser, checksum string, contentLength int64, table *crc32.Table) io.ReadCloser {
 	if checksum == "" {
 		return in
 	}
@@ -78,5 +81,5 @@ func verifyChecksum(in io.ReadCloser, checksum string, contentLength int64) io.R
 		logger.Errorf("invalid crc32c: %s", checksum)
 		return in
 	}
-	return &checksumReader{in, uint32(expected), 0, contentLength}
+	return &checksumReader{in, uint32(expected), 0, contentLength, table}
 }
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/sync/cluster.go b/pkg/sync/cluster.go
index 75575666..06797712 100644
--- a/pkg/sync/cluster.go
+++ b/pkg/sync/cluster.go
@@ -28,6 +28,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"sync"
 	"syscall"
@@ -254,7 +255,7 @@ func launchWorker(address string, config *Config, wg *sync.WaitGroup) {
 				return
 			}
 			rpath := filepath.Join("/tmp", filepath.Base(path))
-			cmd := exec.Command("rsync", "-au", "-e", "ssh -o StrictHostKeyChecking=no", "-e", "ssh -o PasswordAuthentication=no", path, host+":"+rpath)
+			cmd := exec.Command("rsync", "-a", "-e", "ssh -o StrictHostKeyChecking=no", "-e", "ssh -o PasswordAuthentication=no", path, host+":"+rpath)
 			output, err := cmd.CombinedOutput()
 			logger.Debugf("exec: %s,err: %s", cmd.String(), string(output))
 			if err != nil {
@@ -308,6 +309,7 @@ func launchWorker(address string, config *Config, wg *sync.WaitGroup) {
 			}
 			logger.Infof("launch a worker on %s", host)
 			var finished = make(chan struct{})
+			var logRe = regexp.MustCompile(`^.*<([A-Z]+)>: (.*)`)
 			go func() {
 				r := bufio.NewReader(stderr)
 				for {
@@ -316,7 +318,27 @@ func launchWorker(address string, config *Config, wg *sync.WaitGroup) {
 						finished <- struct{}{}
 						return
 					}
-					println(host, line[:len(line)-1])
+					line = strings.TrimSuffix(line, "\n")
+
+					var level, content string
+					if matches := logRe.FindStringSubmatch(line); len(matches) >= 3 {
+						level = matches[1]
+						content = matches[2]
+					} else {
+						level = "INFO"
+						content = line
+					}
+
+					switch level {
+					case "ERROR":
+						logger.Errorf("[%s] %s", host, content)
+					case "WARNING":
+						logger.Warnf("[%s] %s", host, content)
+					case "DEBUG":
+						logger.Debugf("[%s] %s", host, content)
+					default:
+						logger.Infof("[%s] %s", host, content)
+					}
 				}
 			}()
 			err = cmd.Wait()
diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go
index 3bfff3d8..2ec5b659 100644
--- a/pkg/sync/sync.go
+++ b/pkg/sync/sync.go
@@ -32,6 +32,7 @@ import (
 	"sync"
 	"sync/atomic"
 	"time"
+	"unicode"
 
 	"github.com/juicedata/juicefs/pkg/object"
 	"github.com/juicedata/juicefs/pkg/utils"
@@ -1395,7 +1396,11 @@ func produceFromList(tasks chan<- object.Object, src, dst object.ObjectStorage,
 		if key == "" {
 			continue
 		}
-		prefixs <- key
+		trimKey := strings.TrimRightFunc(key, unicode.IsSpace)
+		if trimKey != key {
+			logger.Infof("found a prefix with a space character:%q", key)
+		}
+		prefixs <- trimKey
 	}
 	close(prefixs)
 
diff --git a/pkg/utils/alloc.go b/pkg/utils/alloc.go
index f714ea0c..40ffcfb4 100644
--- a/pkg/utils/alloc.go
+++ b/pkg/utils/alloc.go
@@ -18,6 +18,7 @@ package utils
 
 import (
 	"fmt"
+	"math/bits"
 	"runtime"
 	"sync"
 	"sync/atomic"
@@ -63,14 +64,13 @@ func AllocMemory() int64 {
 
 var pools []*sync.Pool
 
+// PowerOf2 returns the smallest power of 2 that is >= s
 func PowerOf2(s int) int {
-	var bits int
-	var p int = 1
-	for p < s {
-		bits++
-		p *= 2
+	if s <= 0 {
+		return 0
 	}
-	return bits
+	// Find position of the most significant bit (MSB)
+	return bits.Len(uint(s - 1))
 }
 
 func init() {
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 03368337..c544dc6b 100644
--- a/pkg/winfsp/winfs.go
+++ b/pkg/winfsp/winfs.go
@@ -327,18 +327,24 @@ func (j *juice) Open(path string, flags int) (e int, fh uint64) {
 
 // Open opens a file.
 // The flags are a combination of the fuse.O_* constants.
-func (j *juice) OpenEx(path string, fi *fuse.FileInfo_t) (e int) {
+func (j *juice) OpenEx(p string, fi *fuse.FileInfo_t) (e int) {
 	ctx := j.newContext()
-	defer trace(path, fi.Flags)(&e)
+	defer trace(p, fi.Flags)(&e)
 	ino := meta.Ino(0)
-	if strings.HasSuffix(path, "/.control") {
+	if strings.HasSuffix(p, "/.control") {
 		ino, _ = vfs.GetInternalNodeByName(".control")
 		if ino == 0 {
 			e = -fuse.ENOENT
 			return
 		}
+	} else if filename := path.Base(p); vfs.IsSpecialName(filename) {
+		ino, _ = vfs.GetInternalNodeByName(filename)
+		if ino == 0 {
+			e = -fuse.ENOENT
+			return
+		}
 	} else {
-		f, err := j.fs.Open(ctx, path, 0)
+		f, err := j.fs.Open(ctx, p, 0)
 		if err != 0 {
 			e = -fuse.ENOENT
 			return
@@ -444,7 +450,7 @@ func (j *juice) reopen(p string, fh *uint64) meta.Ino {
 }
 
 // Getattr gets file attributes.
-func (j *juice) getAttrForControlFile(ctx vfs.LogContext, p string, stat *fuse.Stat_t, fh uint64) (e int) {
+func (j *juice) getAttrForSpFile(ctx vfs.LogContext, p string, stat *fuse.Stat_t, fh uint64) (e int) {
 	parentDir := path.Dir(p)
 	_, err := j.fs.Stat(ctx, parentDir)
 	if err != 0 {
@@ -452,7 +458,8 @@ func (j *juice) getAttrForControlFile(ctx vfs.LogContext, p string, stat *fuse.S
 		return
 	}
 
-	inode, attr := vfs.GetInternalNodeByName(".control")
+	filename := path.Base(p)
+	inode, attr := vfs.GetInternalNodeByName(filename)
 	if inode == 0 {
 		e = -fuse.ENOENT
 		return
@@ -475,7 +482,10 @@ func (j *juice) Getattr(p string, stat *fuse.Stat_t, fh uint64) (e int) {
 	if ino == 0 {
 		// special case for .control file
 		if strings.HasSuffix(p, "/.control") {
-			j.getAttrForControlFile(ctx, p, stat, fh)
+			e = j.getAttrForSpFile(ctx, p, stat, fh)
+			return
+		} else if vfs.IsSpecialName(path.Base(p)) {
+			e = j.getAttrForSpFile(ctx, p, stat, fh)
 			return
 		}
 
@@ -751,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.")
 
@@ -806,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 0809cb06..eb1d4c6a 100644
--- a/sdk/java/libjfs/main.go
+++ b/sdk/java/libjfs/main.go
@@ -115,6 +115,7 @@ const (
 	EACCES    = -0x0d
 	EEXIST    = -0x11
 	ENOTDIR   = -0x14
+	EISDIR    = -0x15
 	EINVAL    = -0x16
 	ENOSPC    = -0x1c
 	EDQUOT    = -0x45
@@ -151,6 +152,8 @@ func errno(err error) int32 {
 		return EEXIST
 	case syscall.ENOTDIR:
 		return ENOTDIR
+	case syscall.EISDIR:
+		return EISDIR
 	case syscall.EINVAL:
 		return EINVAL
 	case syscall.ENOSPC:
@@ -811,6 +814,29 @@ func jfs_open(pid int64, h int64, cpath *C.char, lenPtr uintptr, flags int32) in
 	return nextFileHandle(f, w)
 }
 
+//export jfs_open_posix
+func jfs_open_posix(pid int64, h int64, cpath *C.char, lenPtr uintptr, flags int32) int32 {
+	w := F(h)
+	if w == nil {
+		return EINVAL
+	}
+	path := C.GoString(cpath)
+	f, err := w.Open(w.withPid(pid), path, uint32(flags))
+	if err != 0 {
+		return errno(err)
+	}
+	st, _ := f.Stat()
+	if st.IsDir() {
+		return EISDIR
+	}
+	if lenPtr != 0 {
+		buf := toBuf(lenPtr, 8)
+		wb := utils.NewNativeBuffer(buf)
+		wb.Put64(uint64(st.Size()))
+	}
+	return nextFileHandle(f, w)
+}
+
 //export jfs_access
 func jfs_access(pid int64, h int64, cpath *C.char, flags int64) int32 {
 	w := F(h)
@@ -878,6 +904,24 @@ func jfs_delete(pid int64, h int64, cpath *C.char) int32 {
 	return errno(w.Delete(w.withPid(pid), C.GoString(cpath)))
 }
 
+//export jfs_unlink
+func jfs_unlink(pid int64, h int64, cpath *C.char) int32 {
+	w := F(h)
+	if w == nil {
+		return EINVAL
+	}
+	return errno(w.Unlink(w.withPid(pid), C.GoString(cpath)))
+}
+
+//export jfs_rmdir
+func jfs_rmdir(pid int64, h int64, cpath *C.char) int32 {
+	w := F(h)
+	if w == nil {
+		return EINVAL
+	}
+	return errno(w.Rmdir(w.withPid(pid), C.GoString(cpath)))
+}
+
 //export jfs_rmr
 func jfs_rmr(pid int64, h int64, cpath *C.char) int32 {
 	w := F(h)
@@ -1217,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)
 	}
@@ -1228,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)
@@ -1485,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
@@ -1496,23 +1597,26 @@ 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)
+	err := w.Clone(ctx, src, dst, preserve)
+	return errno(err)
+}
+
+//export jfs_status
+func jfs_status(pid int64, h int64, trash bool, session uint64, p_buf **byte) int32 {
+	w := F(h)
+	if w == nil {
+		return EINVAL
 	}
-	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)
+	ctx := w.withPid(pid)
+	res, err := w.Status(ctx, trash, session)
+	if err != nil {
+		logger.Errorf("status %s: %s", ctx, err)
 	}
-	return errno(err)
+
+	*p_buf = (*byte)(C.malloc(C.size_t(len(res))))
+	buf := unsafe.Slice(*p_buf, len(res))
+
+	return int32(copy(buf, res))
 }
 
 //export jfs_lseek
@@ -1654,5 +1758,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 bf482637..6b85f0fe 100644
--- a/sdk/python/juicefs/juicefs/juicefs.py
+++ b/sdk/python/juicefs/juicefs/juicefs.py
@@ -36,7 +36,14 @@ XATTR_REPLACE = 2
 
 def check_error(r, fn, args):
     if r < 0:
-        e = OSError(f'call {fn.__name__} failed: [Errno {-r}] {os.strerror(-r)}: {args[2:]}')
+        formatted_args = []
+        for arg in args[2:]:
+            if isinstance(arg, (bytes, bytearray)) and len(arg) > 1024:
+                formatted_args.append(f'bytes(len={len(arg)})')
+            else:
+                formatted_args.append(repr(arg))
+
+        e = OSError(f'call {fn.__name__} failed: [Errno {-r}] {os.strerror(-r)}: {formatted_args}')
         e.errno = -r
         raise e
     return r
@@ -81,39 +88,39 @@ class JuiceFSLib(object):
 
 class Client(object):
     """A JuiceFS client."""
-    def __init__(self, name, meta, *, bucket="", storage_class="", read_only=False, no_session=False, 
-                 no_bg_job=False, open_cache="", backup_meta="", backup_skip_trash=False, heartbeat="", 
-                 cache_dir="", cache_size="100M", free_space="", auto_create=False, cache_full_block=False, 
-                 cache_checksum="", cache_eviction="", cache_scan_interval="", cache_expire="", 
-                 writeback=False, memory_size="300M", prefetch=0, readahead="100M", upload_limit="10g", 
-                 download_limit="10g", max_uploads=0, max_deletes=0, skip_dir_nlink=0, skip_dir_mtime="", 
-                 io_retries=0, get_timeout="", put_timeout="", fast_resolve=False, attr_timeout="", 
-                 entry_timeout="", dir_entry_timeout="", debug=False, no_usage_report=False, access_log="", 
-                 push_gateway="", push_interval="", push_auth="", push_labels="", push_graphite="", **kwargs): 
+    def __init__(self, name, meta, *, bucket="", storage_class="", read_only=False, no_session=False, no_bgjob=True,
+                 open_cache="0", backup_meta="3600", backup_skip_trash=False, heartbeat="12",
+                 cache_dir="memory", cache_size="100M", free_space_ratio="0.1", cache_partial_only=False,
+                 verify_cache_checksum="full", cache_eviction="2-random", cache_scan_interval="3600", cache_expire="0",
+                 writeback=False, buffer_size="300M", prefetch=1, max_readahead="0", upload_limit="0",
+                 download_limit="0", max_uploads=20, max_deletes=10, skip_dir_nlink=20, skip_dir_mtime="100ms",
+                 io_retries=10, get_timeout="5", put_timeout="60", fast_resolve=False, attr_cache="1s",
+                 entry_cache="0s", dir_entry_cache="1s", debug=False, no_usage_report=False, access_log="",
+                 push_gateway="", push_interval="10", push_auth="", push_labels="", push_graphite="", **kwargs):
         self.lib = JuiceFSLib()
         kwargs["meta"] = meta
         kwargs["bucket"] = bucket
         kwargs["storageClass"] = storage_class
         kwargs["readOnly"] = read_only
         kwargs["noSession"] = no_session
-        kwargs["noBGJob"] = no_bg_job
+        kwargs["noBGJob"] = no_bgjob
         kwargs["openCache"] = open_cache
         kwargs["backupMeta"] = backup_meta
         kwargs["backupSkipTrash"] = backup_skip_trash
         kwargs["heartbeat"] = heartbeat
         kwargs["cacheDir"] = cache_dir
         kwargs["cacheSize"] = cache_size
-        kwargs["freeSpace"] = free_space
-        kwargs["autoCreate"] = auto_create
-        kwargs["cacheFullBlock"] = cache_full_block
-        kwargs["cacheChecksum"] = cache_checksum
+        kwargs["freeSpace"] = free_space_ratio
+        kwargs["autoCreate"] = True
+        kwargs["cacheFullBlock"] = not cache_partial_only
+        kwargs["cacheChecksum"] = verify_cache_checksum
         kwargs["cacheEviction"] = cache_eviction
         kwargs["cacheScanInterval"] = cache_scan_interval
         kwargs["cacheExpire"] = cache_expire
         kwargs["writeback"] = writeback
-        kwargs["memorySize"] = memory_size
+        kwargs["memorySize"] = buffer_size
         kwargs["prefetch"] = prefetch
-        kwargs["readahead"] = readahead
+        kwargs["readahead"] = max_readahead
         kwargs["uploadLimit"] = upload_limit
         kwargs["downloadLimit"] = download_limit
         kwargs["maxUploads"] = max_uploads
@@ -124,9 +131,9 @@ class Client(object):
         kwargs["getTimeout"] = get_timeout
         kwargs["putTimeout"] = put_timeout
         kwargs["fastResolve"] = fast_resolve
-        kwargs["attrTimeout"] = attr_timeout
-        kwargs["entryTimeout"] = entry_timeout
-        kwargs["dirEntryTimeout"] = dir_entry_timeout
+        kwargs["attrTimeout"] = attr_cache
+        kwargs["entryTimeout"] = entry_cache
+        kwargs["dirEntryTimeout"] = dir_entry_cache
         kwargs["debug"] = debug
         kwargs["noUsageReport"] = no_usage_report
         kwargs["accessLog"] = access_log
@@ -199,7 +206,7 @@ class Client(object):
         else:
             try:
                 sz = c_uint64()
-                fd = self.lib.jfs_open(c_int64(_tid()), c_int64(self.h), _bin(path), byref(sz), c_int32(flag))
+                fd = self.lib.jfs_open_posix(c_int64(_tid()), c_int64(self.h), _bin(path), byref(sz), c_int32(flag))
                 if 'w' in mode:
                     self.lib.jfs_ftruncate(c_int64(_tid()), fd, c_uint64(0))
                 else:
@@ -230,7 +237,7 @@ class Client(object):
 
     def rmdir(self, path):
         """Remove a directory. The directory must be empty."""
-        self.lib.jfs_rmr(c_int64(_tid()), c_int64(self.h), _bin(path))
+        self.lib.jfs_rmdir(c_int64(_tid()), c_int64(self.h), _bin(path))
 
     def rename(self, old, new):
         """Rename the file or directory old to new."""
@@ -291,7 +298,7 @@ class Client(object):
 
     def unlink(self, path):
         """Remove a file."""
-        self.remove(path)
+        self.lib.jfs_unlink(c_int64(_tid()), c_int64(self.h), _bin(path))
 
     def rmr(self, path):
         """Remove a directory and all its contents recursively."""
@@ -334,12 +341,61 @@ 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 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()
 
-    # def summary(self, path, depth=0, entries=1):
-    #     """Get the summary of a directory."""
+        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
+
+
+    def status(self, trash = False, session = 0):
+        buf = c_void_p()
+        n = self.lib.jfs_status(c_int64(_tid()), c_int64(self.h), c_bool(trash), c_bool(session), byref(buf))
+        res = json.loads(str(string_at(buf, n), encoding='utf-8'))
+        self.lib.free(buf)
+        return res
 
 class File(object):
     """A JuiceFS file."""
@@ -596,7 +652,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/hypo/fs_sdk_test.py b/.github/scripts/hypo/fs_sdk_test.py
index b1be32e0..75949b77 100644
--- a/.github/scripts/hypo/fs_sdk_test.py
+++ b/.github/scripts/hypo/fs_sdk_test.py
@@ -297,25 +297,57 @@ class TestPySdk(unittest.TestCase):
         state.rename_dir(entry=folders_0, new_entry_name='a/a', parent=folders_1, umask=18, user='root')
         state.teardown()
 
-    # def test_rmdir_check_exist(self):
-    #     state = JuicefsMachine()
-    #     folders_0 = state.init_folders()
-    #     folders_1 = state.mkdir(mode=0, parent=folders_0, subdir='a', umask=18, user='root')
-    #     files_0 = state.loop_symlink(link_file_name=folders_1, parent=folders_1, user='root')
-    #     state.exists(entry=files_0, user='root')
-    #     state.rmdir(dir=folders_1, user='root')
-    #     state.teardown()
-
-    # def test_truncate(self):
-    #     state = JuicefsMachine()
-    #     folders_0 = state.init_folders()
-    #     files_0 = state.loop_symlink(link_file_name='a', parent=folders_0, user='root')
-    #     files_1 = state.rename_file(entry=files_0, new_entry_name='b', parent=folders_0, umask=18, user='root')
-    #     state.listdir(dir=folders_0, user='root')
-    #     folders_1 = state.mkdir(mode=0, parent=folders_0, subdir=files_0, umask=18, user='root')
-    #     state.listdir(dir=files_0, user='root')
-    #     state.truncate(file=files_0, size=0, user='root')
-    #     state.teardown()
+    def test_rmdir_check_exist(self):
+        state = JuicefsMachine()
+        folders_0 = state.init_folders()
+        folders_1 = state.mkdir(mode=0, parent=folders_0, subdir='a', umask=18, user='root')
+        files_0 = state.loop_symlink(link_file_name=folders_1, parent=folders_1, user='root')
+        state.exists(entry=files_0, user='root')
+        state.rmdir(dir=folders_1, user='root')
+        state.teardown()
+
+    def test_truncate(self):
+        state = JuicefsMachine()
+        folders_0 = state.init_folders()
+        files_0 = state.loop_symlink(link_file_name='a', parent=folders_0, user='root')
+        files_1 = state.rename_file(entry=files_0, new_entry_name='b', parent=folders_0, umask=18, user='root')
+        state.listdir(dir=folders_0, user='root')
+        folders_1 = state.mkdir(mode=0, parent=folders_0, subdir=files_0, umask=18, user='root')
+        state.listdir(dir=files_0, user='root')
+        state.truncate(file=files_0, size=0, user='root')
+        state.teardown()
+
+    def test_unlink(self):
+        state = JuicefsMachine()
+        folders_0 = state.init_folders()
+        state.listdir(dir=folders_0, user='root')
+        files_0 = state.loop_symlink(link_file_name='a', parent=folders_0, user='root')
+        files_1 = state.rename_file(entry=files_0, new_entry_name='b', parent=folders_0, umask=18, user='root')
+        folders_1 = state.mkdir(mode=0, parent=folders_0, subdir=files_0, umask=18, user='root')
+        state.unlink(file=files_0, user='root')
+        state.teardown()
+
+    def test_read_utf8(self):
+        state = JuicefsMachine()
+        folders_0 = state.init_folders()
+        files_0 = state.create_file(content=b'', file_name='aa', parent=folders_0, umask=18, user='root')
+        folders_1 = state.mkdir(mode=0, parent=folders_0, subdir='a', umask=18, user='root')
+        state.rename_dir(entry=folders_0, new_entry_name='a/a', parent=folders_1, umask=18, user='root')
+        state.teardown()
+
+    def test_create_isdir(self):
+        state = JuicefsMachine()
+        folders_0 = state.init_folders()
+        state.listdir(dir=folders_0, user='root')
+        files_0 = state.loop_symlink(link_file_name='a', parent=folders_0, user='root')
+        state.chmod(entry=files_0, mode=0, user='root')
+        files_1 = state.rename_file(entry=files_0, new_entry_name='b', parent=folders_0, umask=18, user='root')
+        folders_1 = state.mkdir(mode=0, parent=folders_0, subdir=files_0, umask=18, user='root')
+        state.truncate(file=files_0, size=0, user='root')
+        state.readlink(file=files_0, user='root')
+        state.writelines(file=files_0, lines=['dua', 'hbixuhv'], mode='wb', offset=8344, user='root', whence=1)
+        state.teardown()
+
 
 if __name__ == '__main__':
     unittest.main()
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/.github/workflows/integrationtests.yml b/.github/workflows/integrationtests.yml
index 04b22d55..60de09fa 100644
--- a/.github/workflows/integrationtests.yml
+++ b/.github/workflows/integrationtests.yml
@@ -96,7 +96,7 @@ jobs:
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
-          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url --trash-days 0 pics
+          sudo GOCOVERDIR=$(pwd)/cover ./juicefs format $meta_url --trash-days 0 --bucket=/mnt/jfs pics
 
       - name: Juicefs Mount
         run: |
diff --git a/.github/workflows/mutate-test-sdk.yml b/.github/workflows/mutate-test-sdk.yml
index 54663c38..a98cd47a 100644
--- a/.github/workflows/mutate-test-sdk.yml
+++ b/.github/workflows/mutate-test-sdk.yml
@@ -50,7 +50,7 @@ jobs:
 
       - name: Juicefs Format
         run: |
-          sudo ./juicefs format  localhost dev
+          sudo ./juicefs format  localhost --bucket=/mnt/jfs dev
 
       - name: Juicefs Mount
         run: |
diff --git a/.github/workflows/pjdfstest.yml b/.github/workflows/pjdfstest.yml
index 719589ec..ea83ced9 100644
--- a/.github/workflows/pjdfstest.yml
+++ b/.github/workflows/pjdfstest.yml
@@ -102,7 +102,7 @@ jobs:
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
-          sudo ./juicefs format $meta_url --trash-days 0 pics
+          sudo ./juicefs format $meta_url --trash-days 0 --bucket=/mnt/jfs pics
 
       - name: Juicefs Mount
         run: |
diff --git a/.github/workflows/sdktest.yml b/.github/workflows/sdktest.yml
index a8977eb3..90d0a32e 100644
--- a/.github/workflows/sdktest.yml
+++ b/.github/workflows/sdktest.yml
@@ -54,7 +54,7 @@ jobs:
 
       - name: Juicefs Format
         run: |
-          sudo ./juicefs format  localhost dev
+          sudo ./juicefs format  localhost  --bucket=/mnt/jfs dev
 
       - name: Juicefs Mount
         run: |
diff --git a/.github/workflows/wintest.yml b/.github/workflows/wintest.yml
index 35d8c00d..189dcdfc 100644
--- a/.github/workflows/wintest.yml
+++ b/.github/workflows/wintest.yml
@@ -7,12 +7,16 @@ on:
       - 'release-**'
     paths:
       - '**/wintest.yml'
+      - 'pkg/winfsp/*.go'
+      - '**/*_windows.go'
   pull_request:
     branches:
       - 'main'
       - 'release-**'
     paths:
       - '**/wintest.yml'
+      - 'pkg/winfsp/*.go'
+      - '**/*_windows.go'
   workflow_dispatch:
     inputs:
       debug_enabled:
@@ -153,7 +157,19 @@ jobs:
           wget https://github.com/chenjie4255/fstools/releases/download/v0.0.1/fsx-x64.exe -q --show-progress -O fsx.exe
           ls fsx.exe
           ./fsx.exe -d 180 -p 10000 -F 100000 fsxtest
-          
+
+      - name: Run basic subcommand tests
+        run: |
+          echo hi > Z:\hi.txt
+          ./juicefs.exe info Z:\hi.txt
+          ./juicefs.exe status redis://127.0.0.1:6379/1
+          ./juicefs.exe debug Z:\
+          New-Item -Path 'Z:\summary' -ItemType Directory
+          echo hi > Z:\summary\1.txt
+          echo hi > Z:\summary\2.txt
+          ./juicefs.exe summary Z:\summary
+          ./juicefs.exe info Z:\summary\1.txt
+          ./juicefs.exe stats Z: -c 5
       - name: Setup tmate session
         if: ${{ failure() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
         uses: mxschmitt/action-tmate@v3
diff --git a/cmd/status_test.go b/cmd/status_test.go
index 9f611a74..4216f39b 100644
--- a/cmd/status_test.go
+++ b/cmd/status_test.go
@@ -22,8 +22,30 @@ import (
 	"testing"
 
 	"github.com/agiledragon/gomonkey/v2"
+	"github.com/juicedata/juicefs/pkg/meta"
 )
 
+type statistic struct {
+	UsedSpace                uint64
+	AvailableSpace           uint64
+	UsedInodes               uint64
+	AvailableInodes          uint64
+	TrashFileCount           int64 `json:",omitempty"`
+	TrashFileSize            int64 `json:",omitempty"`
+	PendingDeletedFileCount  int64 `json:",omitempty"`
+	PendingDeletedFileSize   int64 `json:",omitempty"`
+	TrashSliceCount          int64 `json:",omitempty"`
+	TrashSliceSize           int64 `json:",omitempty"`
+	PendingDeletedSliceCount int64 `json:",omitempty"`
+	PendingDeletedSliceSize  int64 `json:",omitempty"`
+}
+
+type sections struct {
+	Setting   *meta.Format
+	Sessions  []*meta.Session
+	Statistic *statistic
+}
+
 func TestStatus(t *testing.T) {
 	tmpFile, err := os.CreateTemp("/tmp", "")
 	if err != nil {
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/utils/alloc_test.go b/pkg/utils/alloc_test.go
index 2c0b0354..46d76fbf 100644
--- a/pkg/utils/alloc_test.go
+++ b/pkg/utils/alloc_test.go
@@ -16,7 +16,9 @@
 
 package utils
 
-import "testing"
+import (
+	"testing"
+)
 
 func TestAlloc(t *testing.T) {
 	old := AllocMemory()
@@ -29,3 +31,31 @@ func TestAlloc(t *testing.T) {
 		t.Fatalf("free all allocated memory, but got %d", AllocMemory()-old)
 	}
 }
+
+func PowerOf2Loop(s int) int {
+	var bits int
+	var p int = 1
+	for p < s {
+		bits++
+		p *= 2
+	}
+	return bits
+}
+
+func BenchmarkPowerOf2(b *testing.B) {
+	b.Run("bits.Len", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			for j := 0; j < 100000; j++ {
+				_ = PowerOf2(j)
+			}
+		}
+	})
+
+	b.Run("Loop", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			for j := 0; j < 100000; j++ {
+				_ = PowerOf2Loop(j)
+			}
+		}
+	})
+}
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)
 }