Repository: AlistGo/alist (language: go)

Issue:
Crypt 降速

### Discussed in https://github.com/AlistGo/alist/discussions/7995

<div type='discussions-op-text'>

<sup>Originally posted by **sususu98** February 14, 2025</sup>
使用Crypt，后端连接夸克网盘，发现如果文件小于等于392M，下载速度就没问题，如果大于392M就会出现降速情况
![image](https://github.com/user-attachments/assets/bc08e26f-1288-4cb7-b0e7-45d88403f17b)
![image](https://github.com/user-attachments/assets/1823a176-bc08-41c4-9a2c-028d4409babe)

直接下载密文就没有类似的问题，甚至下载更大的文件（399M）也不会出现降速的情况
![image](https://github.com/user-attachments/assets/01dce945-8cfc-48bd-b089-bf878ed61ea9)
![image](https://github.com/user-attachments/assets/8e4c5214-3578-4aaf-97d1-ed1275e9addb)


</div>


Discussion / hints:
### See
1. 50% #7248
2. 50% #6607

<!-- Created by actions-cool/issues-similarity-analysis. Do not remove. -->
我使用相同的密码在rclone中挂载alist的webdav，下载速度没有出现降速的问题

![Image](https://github.com/user-attachments/assets/998db2cf-74a1-4d8c-bd03-146b3915dc7a)
如果使用IDM下载，多线程下载（我理解是被分割为8份，分别请求），下载速度也没有问题。
![Image](https://github.com/user-attachments/assets/3958f52c-45cc-4999-ad12-efc766c46859)

Fix patch:
diff --git a/drivers/alias/util.go b/drivers/alias/util.go
index ee17b622e13..2157a43d7eb 100644
--- a/drivers/alias/util.go
+++ b/drivers/alias/util.go
@@ -63,6 +63,7 @@ func (d *Alias) get(ctx context.Context, path string, dst, sub string) (model.Ob
 		Size:     obj.GetSize(),
 		Modified: obj.ModTime(),
 		IsFolder: obj.IsDir(),
+		HashInfo: obj.GetHash(),
 	}, nil
 }
 
diff --git a/drivers/crypt/driver.go b/drivers/crypt/driver.go
index e6f253d187a..59b25806ecf 100644
--- a/drivers/crypt/driver.go
+++ b/drivers/crypt/driver.go
@@ -263,12 +263,7 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
 		}
 		rrc := remoteLink.RangeReadCloser
 		if len(remoteLink.URL) > 0 {
-
-			rangedRemoteLink := &model.Link{
-				URL:    remoteLink.URL,
-				Header: remoteLink.Header,
-			}
-			var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, rangedRemoteLink)
+			var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, remoteLink)
 			if err != nil {
 				return nil, err
 			}
@@ -304,7 +299,6 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
 
 	resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers}
 	resultLink := &model.Link{
-		Header:          remoteLink.Header,
 		RangeReadCloser: resultRangeReadCloser,
 		Expiration:      remoteLink.Expiration,
 	}
diff --git a/drivers/quark_uc/util.go b/drivers/quark_uc/util.go
index 9a3bdc1c00a..c5845cc6823 100644
--- a/drivers/quark_uc/util.go
+++ b/drivers/quark_uc/util.go
@@ -170,7 +170,7 @@ x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit
 	if res.StatusCode() != 200 {
 		return "", fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String())
 	}
-	return res.Header().Get("ETag"), nil
+	return res.Header().Get("Etag"), nil
 }
 
 func (d *QuarkOrUC) upCommit(pre UpPreResp, md5s []string) error {
diff --git a/drivers/quqi/util.go b/drivers/quqi/util.go
index c57e641bf1e..5ad43c4b97b 100644
--- a/drivers/quqi/util.go
+++ b/drivers/quqi/util.go
@@ -304,10 +304,6 @@ func (d *Quqi) linkFromCDN(id string) (*model.Link, error) {
 	}
 
 	return &model.Link{
-		Header: http.Header{
-			"Origin": []string{"https://quqi.com"},
-			"Cookie": []string{d.Cookie},
-		},
 		RangeReadCloser: &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers},
 		Expiration:      &expiration,
 	}, nil
diff --git a/internal/net/request.go b/internal/net/request.go
index d2f3028fac9..c9ef363f2e0 100644
--- a/internal/net/request.go
+++ b/internal/net/request.go
@@ -382,6 +382,9 @@ func (d *downloader) tryDownloadChunk(params *HttpRequestParams, ch *chunk) (int
 		if resp == nil {
 			return 0, err
 		}
+		if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
+			return 0, err
+		}
 		if ch.id == 0 { //第1个任务 有限的重试，超过重试就会结束请求
 			switch resp.StatusCode {
 			default:
diff --git a/internal/net/serve.go b/internal/net/serve.go
index c75e611f8d5..8b6b3d1d234 100644
--- a/internal/net/serve.go
+++ b/internal/net/serve.go
@@ -114,7 +114,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
 
 	// 使用请求的Context
 	// 不然从sendContent读不到数据，即使请求断开CopyBuffer也会一直堵塞
-	ctx := r.Context()
+	ctx := context.WithValue(r.Context(), "request_header", &r.Header)
 	switch {
 	case len(ranges) == 0:
 		reader, err := RangeReadCloser.RangeRead(ctx, http_range.Range{Length: -1})
diff --git a/internal/net/util.go b/internal/net/util.go
index 45301dde9f2..5b335a7f718 100644
--- a/internal/net/util.go
+++ b/internal/net/util.go
@@ -71,6 +71,7 @@ func checkIfMatch(w http.ResponseWriter, r *http.Request) condResult {
 	if im == "" {
 		return condNone
 	}
+	r.Header.Del("If-Match")
 	for {
 		im = textproto.TrimString(im)
 		if len(im) == 0 {
@@ -98,7 +99,11 @@ func checkIfMatch(w http.ResponseWriter, r *http.Request) condResult {
 
 func checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult {
 	ius := r.Header.Get("If-Unmodified-Since")
-	if ius == "" || isZeroTime(modtime) {
+	if ius == "" {
+		return condNone
+	}
+	r.Header.Del("If-Unmodified-Since")
+	if isZeroTime(modtime) {
 		return condNone
 	}
 	t, err := http.ParseTime(ius)
@@ -120,6 +125,7 @@ func checkIfNoneMatch(w http.ResponseWriter, r *http.Request) condResult {
 	if inm == "" {
 		return condNone
 	}
+	r.Header.Del("If-None-Match")
 	buf := inm
 	for {
 		buf = textproto.TrimString(buf)
@@ -150,7 +156,11 @@ func checkIfModifiedSince(r *http.Request, modtime time.Time) condResult {
 		return condNone
 	}
 	ims := r.Header.Get("If-Modified-Since")
-	if ims == "" || isZeroTime(modtime) {
+	if ims == "" {
+		return condNone
+	}
+	r.Header.Del("If-Modified-Since")
+	if isZeroTime(modtime) {
 		return condNone
 	}
 	t, err := http.ParseTime(ims)
@@ -174,6 +184,7 @@ func checkIfRange(w http.ResponseWriter, r *http.Request, modtime time.Time) con
 	if ir == "" {
 		return condNone
 	}
+	r.Header.Del("If-Range")
 	etag, _ := scanETag(ir)
 	if etag != "" {
 		if etagStrongMatch(etag, w.Header().Get("Etag")) {
diff --git a/internal/stream/stream.go b/internal/stream/stream.go
index 5eb6bdc7a4c..1c94715f95a 100644
--- a/internal/stream/stream.go
+++ b/internal/stream/stream.go
@@ -384,7 +384,7 @@ func (c *headCache) read(p []byte) (n int, err error) {
 			n, err = lr.Read(buf[off:])
 			off += n
 			c.cur += int64(n)
-			if err == io.EOF && n == int(bufL) {
+			if err == io.EOF && off == int(bufL) {
 				err = nil
 			}
 			if err != nil {
@@ -468,7 +468,7 @@ func (r *RangeReadReadAtSeeker) getReaderAtOffset(off int64) (*readerCur, error)
 		}
 	}
 	if rc != nil && off-rc.cur <= utils.MB {
-		n, err := utils.CopyWithBufferN(utils.NullWriter{}, rc.reader, off-rc.cur)
+		n, err := utils.CopyWithBufferN(io.Discard, rc.reader, off-rc.cur)
 		rc.cur += n
 		if err == io.EOF && rc.cur == off {
 			err = nil
diff --git a/internal/stream/util.go b/internal/stream/util.go
index bb5019e0df2..b2c76754040 100644
--- a/internal/stream/util.go
+++ b/internal/stream/util.go
@@ -3,13 +3,13 @@ package stream
 import (
 	"context"
 	"fmt"
-	"github.com/alist-org/alist/v3/pkg/utils"
 	"io"
 	"net/http"
 
 	"github.com/alist-org/alist/v3/internal/model"
 	"github.com/alist-org/alist/v3/internal/net"
 	"github.com/alist-org/alist/v3/pkg/http_range"
+	"github.com/alist-org/alist/v3/pkg/utils"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -19,7 +19,11 @@ func GetRangeReadCloserFromLink(size int64, link *model.Link) (model.RangeReadCl
 	}
 	rangeReaderFunc := func(ctx context.Context, r http_range.Range) (io.ReadCloser, error) {
 		if link.Concurrency != 0 || link.PartSize != 0 {
-			header := net.ProcessHeader(http.Header{}, link.Header)
+			requestHeader := ctx.Value("request_header")
+			if requestHeader == nil {
+				requestHeader = &http.Header{}
+			}
+			header := net.ProcessHeader(*(requestHeader.(*http.Header)), link.Header)
 			down := net.NewDownloader(func(d *net.Downloader) {
 				d.Concurrency = link.Concurrency
 				d.PartSize = link.PartSize
@@ -60,7 +64,11 @@ func GetRangeReadCloserFromLink(size int64, link *model.Link) (model.RangeReadCl
 }
 
 func RequestRangedHttp(ctx context.Context, link *model.Link, offset, length int64) (*http.Response, error) {
-	header := net.ProcessHeader(http.Header{}, link.Header)
+	requestHeader := ctx.Value("request_header")
+	if requestHeader == nil {
+		requestHeader = &http.Header{}
+	}
+	header := net.ProcessHeader(*(requestHeader.(*http.Header)), link.Header)
 	header = http_range.ApplyRangeToHttpHeader(http_range.Range{Start: offset, Length: length}, header)
 
 	return net.RequestHttp(ctx, "GET", header, link.URL)
diff --git a/pkg/utils/io.go b/pkg/utils/io.go
index c314307d51d..e06fb235b8b 100644
--- a/pkg/utils/io.go
+++ b/pkg/utils/io.go
@@ -233,9 +233,3 @@ func CopyWithBufferN(dst io.Writer, src io.Reader, n int64) (written int64, err
 	}
 	return
 }
-
-type NullWriter struct{}
-
-func (NullWriter) Write(p []byte) (n int, err error) {
-	return len(p), nil
-}
diff --git a/server/common/proxy.go b/server/common/proxy.go
index 66854976b08..8519ed53c44 100644
--- a/server/common/proxy.go
+++ b/server/common/proxy.go
@@ -19,7 +19,7 @@ import (
 func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.Obj) error {
 	if link.MFile != nil {
 		defer link.MFile.Close()
-		attachFileName(w, file)
+		attachHeader(w, file)
 		contentType := link.Header.Get("Content-Type")
 		if contentType != "" {
 			w.Header().Set("Content-Type", contentType)
@@ -35,17 +35,21 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.
 		http.ServeContent(w, r, file.GetName(), file.ModTime(), mFile)
 		return nil
 	} else if link.RangeReadCloser != nil {
-		attachFileName(w, file)
+		attachHeader(w, file)
 		net.ServeHTTP(w, r, file.GetName(), file.ModTime(), file.GetSize(), &stream.RateLimitRangeReadCloser{
 			RangeReadCloserIF: link.RangeReadCloser,
 			Limiter:           stream.ServerDownloadLimit,
 		})
 		return nil
 	} else if link.Concurrency != 0 || link.PartSize != 0 {
-		attachFileName(w, file)
+		attachHeader(w, file)
 		size := file.GetSize()
-		header := net.ProcessHeader(r.Header, link.Header)
 		rangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
+			requestHeader := ctx.Value("request_header")
+			if requestHeader == nil {
+				requestHeader = &http.Header{}
+			}
+			header := net.ProcessHeader(*(requestHeader.(*http.Header)), link.Header)
 			down := net.NewDownloader(func(d *net.Downloader) {
 				d.Concurrency = link.Concurrency
 				d.PartSize = link.PartSize
@@ -91,10 +95,20 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.
 		return nil
 	}
 }
-func attachFileName(w http.ResponseWriter, file model.Obj) {
+func attachHeader(w http.ResponseWriter, file model.Obj) {
 	fileName := file.GetName()
 	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, fileName, url.PathEscape(fileName)))
 	w.Header().Set("Content-Type", utils.GetMimeType(fileName))
+	w.Header().Set("Etag", GetEtag(file))
+}
+func GetEtag(file model.Obj) string {
+	for _, v := range file.GetHash().Export() {
+		if len(v) != 0 {
+			return fmt.Sprintf(`"%s"`, v)
+		}
+	}
+	// 参考nginx
+	return fmt.Sprintf(`"%x-%x"`, file.ModTime().Unix(), file.GetSize())
 }
 
 var NoProxyRange = &model.RangeReadCloser{}
diff --git a/server/s3/backend.go b/server/s3/backend.go
index bca45008cde..a1e990441be 100644
--- a/server/s3/backend.go
+++ b/server/s3/backend.go
@@ -195,11 +195,7 @@ func (b *s3Backend) GetObject(ctx context.Context, bucketName, objectName string
 		}
 		rrc := link.RangeReadCloser
 		if len(link.URL) > 0 {
-			rangedRemoteLink := &model.Link{
-				URL:    link.URL,
-				Header: link.Header,
-			}
-			var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, rangedRemoteLink)
+			var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, link)
 			if err != nil {
 				return nil, err
 			}
diff --git a/server/webdav/prop.go b/server/webdav/prop.go
index b1474ea3e95..5e053af4b1a 100644
--- a/server/webdav/prop.go
+++ b/server/webdav/prop.go
@@ -9,7 +9,6 @@ import (
 	"context"
 	"encoding/xml"
 	"errors"
-	"fmt"
 	"mime"
 	"net/http"
 	"path"
@@ -18,6 +17,7 @@ import (
 	"time"
 
 	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/server/common"
 )
 
 // Proppatch describes a property update instruction as defined in RFC 4918.
@@ -473,7 +473,7 @@ func findETag(ctx context.Context, ls LockSystem, name string, fi model.Obj) (st
 	// The Apache http 2.4 web server by default concatenates the
 	// modification time and size of a file. We replicate the heuristic
 	// with nanosecond granularity.
-	return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.GetSize()), nil
+	return common.GetEtag(fi), nil
 }
 
 func findSupportedLock(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
diff --git a/server/webdav/webdav.go b/server/webdav/webdav.go
index 6585056bbde..1b7ec6ff72b 100644
--- a/server/webdav/webdav.go
+++ b/server/webdav/webdav.go
@@ -227,11 +227,6 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
 	if err != nil {
 		return http.StatusNotFound, err
 	}
-	etag, err := findETag(ctx, h.LockSystem, reqPath, fi)
-	if err != nil {
-		return http.StatusInternalServerError, err
-	}
-	w.Header().Set("ETag", etag)
 	if r.Method == http.MethodHead {
 		w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.GetSize()))
 		return http.StatusOK, nil
@@ -361,7 +356,7 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
 	if err != nil {
 		return http.StatusInternalServerError, err
 	}
-	w.Header().Set("ETag", etag)
+	w.Header().Set("Etag", etag)
 	return http.StatusCreated, nil
 }
 


Test patch: