Repository: AlistGo/alist (language: go)

Issue:
当从local存储copy大文件至其他Alistv3存储时，内存暴增甚至会导致crash
### Please make sure of the following things

- [x] I have read the [documentation](https://alist.nn.ci).
我已经阅读了[文档](https://alist.nn.ci)。

- [x] I'm sure there are no duplicate issues or discussions.
我确定没有重复的issue或讨论。

- [x] I'm sure it's due to `AList` and not something else(such as [Network](https://alist.nn.ci/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`).
我确定是`AList`的问题，而不是其他原因（例如[网络](https://alist.nn.ci/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host)，`依赖`或`操作`）。

- [x] I'm sure this issue is not fixed in the latest version.
我确定这个问题在最新版本中没有被修复。


### AList Version / AList 版本

v3.35.0

### Driver used / 使用的存储驱动

Local & Alist v3

### Describe the bug / 问题描述

同局域网下多个不同类型的设备部署了Alist，当尝试从local copy一些大文件去到远端的alist v3的时候，会观察到alist内存占用暴增，甚至超过正在拷贝的文件大小的增幅。在一些内存较小的设备上会导致进程crash，进而copy失败。为提issue在台式机上windows平台的alist做演示，目标端为android的alist_flutter。同时验证过crash会出现在以android的alist和docker的alist等内存较小的设备上作为源端向远端alist v3的storage copy时会crash。

### Reproduction / 复现链接

https://alist.nn.ci/zh/guide/api/fs.html#post-%E5%A4%8D%E5%88%B6%E6%96%87%E4%BB%B6

### Config / 配置


![image](https://github.com/alist-org/alist/assets/72076542/30adaeb9-4931-4d41-bb92-961d13c86329)



### Logs / 日志

![image](https://github.com/alist-org/alist/assets/72076542/08967b44-74dd-4fd7-ae88-902c4e9d7f32)
![image](https://github.com/alist-org/alist/assets/72076542/5039dff5-3425-470d-88f2-27f4b957d0df)


Discussion / hints:
Thanks for opening your first issue here! Be sure to follow the issue template!

问题出现在`Alist v3`驱动，用的 https://github.com/go-resty/resty 这个库实现的上传
一番debug后定位到[go-resty/resty/middleware.go#L490](https://github.com/go-resty/resty/blob/v2.11.0/middleware.go#L490)
go-resty/resty这个库为了支持重试会缓存io.Reader类型，执行[go-resty/resty/middleware.go#L490](https://github.com/go-resty/resty/blob/v2.11.0/middleware.go#L490)之后内存就会暴增
想了2种解决方法

* 1 改用 go 自带的 net/http ，但不支持重试（需要支持重试的话，要预先缓存到文件，不需要计算哈希值感觉没必要缓存）
* 2 继续使用 github.com/go-resty/resty 然后改为分片上传，需要alist支持

我试了第1个方法，内存不会暴增
@xhofe 有更好的解决方法吗？



Fix patch:
diff --git a/drivers/alist_v3/driver.go b/drivers/alist_v3/driver.go
index 53fb93caa1f..d078c5fb421 100644
--- a/drivers/alist_v3/driver.go
+++ b/drivers/alist_v3/driver.go
@@ -6,9 +6,7 @@ import (
 	"io"
 	"net/http"
 	"path"
-	"strconv"
 	"strings"
-	"time"
 
 	"github.com/alist-org/alist/v3/drivers/base"
 	"github.com/alist-org/alist/v3/internal/conf"
@@ -17,6 +15,7 @@ import (
 	"github.com/alist-org/alist/v3/pkg/utils"
 	"github.com/alist-org/alist/v3/server/common"
 	"github.com/go-resty/resty/v2"
+	log "github.com/sirupsen/logrus"
 )
 
 type AListV3 struct {
@@ -42,7 +41,7 @@ func (d *AListV3) Init(ctx context.Context) error {
 		return err
 	}
 	// if the username is not empty and the username is not the same as the current username, then login again
-	if d.Username != "" && d.Username != resp.Data.Username {
+	if d.Username != resp.Data.Username {
 		err = d.login()
 		if err != nil {
 			return err
@@ -183,14 +182,41 @@ func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
 }
 
 func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
-	_, err := d.requestWithTimeout("/fs/put", http.MethodPut, func(req *resty.Request) {
-		req.SetHeader("File-Path", path.Join(dstDir.GetPath(), stream.GetName())).
-			SetHeader("Password", d.MetaPassword).
-			SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).
-			SetContentLength(true).
-			SetBody(io.ReadCloser(stream))
-	}, time.Hour*6)
-	return err
+	req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", stream)
+	if err != nil {
+		return err
+	}
+	req.Header.Set("Authorization", d.Token)
+	req.Header.Set("File-Path", path.Join(dstDir.GetPath(), stream.GetName()))
+	req.Header.Set("Password", d.MetaPassword)
+
+	req.ContentLength = stream.GetSize()
+	// client := base.NewHttpClient()
+	// client.Timeout = time.Hour * 6
+	res, err := base.HttpClient.Do(req)
+	if err != nil {
+		return err
+	}
+
+	bytes, err := io.ReadAll(res.Body)
+	if err != nil {
+		return err
+	}
+	log.Debugf("[alist_v3] response body: %s", string(bytes))
+	if res.StatusCode >= 400 {
+		return fmt.Errorf("request failed, status: %s", res.Status)
+	}
+	code := utils.Json.Get(bytes, "code").ToInt()
+	if code != 200 {
+		if code == 401 || code == 403 {
+			err = d.login()
+			if err != nil {
+				return err
+			}
+		}
+		return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString())
+	}
+	return nil
 }
 
 //func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
diff --git a/drivers/alist_v3/util.go b/drivers/alist_v3/util.go
index 978f3ac0568..5ede285af5b 100644
--- a/drivers/alist_v3/util.go
+++ b/drivers/alist_v3/util.go
@@ -3,7 +3,6 @@ package alist_v3
 import (
 	"fmt"
 	"net/http"
-	"time"
 
 	"github.com/alist-org/alist/v3/drivers/base"
 	"github.com/alist-org/alist/v3/internal/op"
@@ -14,6 +13,9 @@ import (
 )
 
 func (d *AListV3) login() error {
+	if d.Username == "" {
+		return nil
+	}
 	var resp common.Resp[LoginResp]
 	_, err := d.request("/auth/login", http.MethodPost, func(req *resty.Request) {
 		req.SetResult(&resp).SetBody(base.Json{
@@ -57,33 +59,3 @@ func (d *AListV3) request(api, method string, callback base.ReqCallback, retry .
 	}
 	return res.Body(), nil
 }
-
-func (d *AListV3) requestWithTimeout(api, method string, callback base.ReqCallback, timeout time.Duration, retry ...bool) ([]byte, error) {
-	url := d.Address + "/api" + api
-	client := base.NewRestyClient().SetTimeout(timeout)
-	req := client.R()
-	req.SetHeader("Authorization", d.Token)
-	if callback != nil {
-		callback(req)
-	}
-	res, err := req.Execute(method, url)
-	if err != nil {
-		return nil, err
-	}
-	log.Debugf("[alist_v3] response body: %s", res.String())
-	if res.StatusCode() >= 400 {
-		return nil, fmt.Errorf("request failed, status: %s", res.Status())
-	}
-	code := utils.Json.Get(res.Body(), "code").ToInt()
-	if code != 200 {
-		if (code == 401 || code == 403) && !utils.IsBool(retry...) {
-			err = d.login()
-			if err != nil {
-				return nil, err
-			}
-			return d.requestWithTimeout(api, method, callback, timeout, true)
-		}
-		return nil, fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString())
-	}
-	return res.Body(), nil
-}


Test patch: