Repository: AlistGo/alist (language: go)

Issue:
Multipart file was closed before reading when use form+asTask to upload
### 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 版本

7c6dec5d47081183e93a261bb262a9f557f3c9d1

### Driver used / 使用的存储驱动

unrelated

### Describe the bug / 问题描述

When using form + asTask to upload on the web page, due to the multi-part file implementing the `model.File` interface, there are no temp file created.

However the multi-part file was closed after the HTTP request, so the file was unable to upload the corresponding storage.

### Reproduction / 复现链接

---

### Config / 配置

---

### Logs / 日志

read xxxxx/multipart-xxx.file: file already closed; Has read error


Discussion / hints:

Fix patch:
diff --git a/drivers/aliyundrive_open/upload.go b/drivers/aliyundrive_open/upload.go
index 1f96a9d573b..29e7e310374 100644
--- a/drivers/aliyundrive_open/upload.go
+++ b/drivers/aliyundrive_open/upload.go
@@ -148,7 +148,7 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
 	// Part Size Unit: Bytes, Default: 20MB,
 	// Maximum number of slices 10,000, ≈195.3125GB
 	var partSize = calPartSize(stream.GetSize())
-	const dateFormat = "2006-01-02T15:04:05.88Z"
+	const dateFormat = "2006-01-02T15:04:05.999Z"
 	mtime := stream.ModTime()
 	mtimeStr := mtime.UTC().Format(dateFormat)
 	ctimeStr := stream.CreateTime().UTC().Format(dateFormat)
diff --git a/internal/stream/stream.go b/internal/stream/stream.go
index 957fb6f6e2e..8bc27b3a4d3 100644
--- a/internal/stream/stream.go
+++ b/internal/stream/stream.go
@@ -55,7 +55,6 @@ func (f *FileStream) SetExist(obj model.Obj) {
 
 // CacheFullInTempFile save all data into tmpFile. Not recommended since it wears disk,
 // and can't start upload until the file is written. It's not thread-safe!
-// won't check if some
 func (f *FileStream) CacheFullInTempFile() (model.File, error) {
 	if f.tmpFile != nil {
 		return f.tmpFile, nil
@@ -82,28 +81,28 @@ func (f *FileStream) RangeRead(httpRange http_range.Range) (io.Reader, error) {
 	if httpRange.Length == -1 {
 		httpRange.Length = f.GetSize()
 	}
-
 	if f.peekBuff != nil && httpRange.Start < int64(f.peekBuff.Len()) && httpRange.Start+httpRange.Length-1 < int64(f.peekBuff.Len()) {
 		return io.NewSectionReader(f.peekBuff, httpRange.Start, httpRange.Length), nil
 	}
-	if httpRange.Start == 0 && httpRange.Length <= InMemoryBufMaxSizeBytes && f.peekBuff == nil {
-		bufSize := utils.Min(httpRange.Length, f.GetSize())
-		newBuf := bytes.NewBuffer(make([]byte, 0, bufSize))
-		n, err := io.CopyN(newBuf, f.Reader, bufSize)
-		if err != nil {
-			return nil, err
-		}
-		if n != bufSize {
-			return nil, fmt.Errorf("stream RangeRead did not get all data in peek, expect =%d ,actual =%d", bufSize, n)
-		}
-		f.peekBuff = bytes.NewReader(newBuf.Bytes())
-		f.Reader = io.MultiReader(f.peekBuff, f.Reader)
-		return io.NewSectionReader(f.peekBuff, httpRange.Start, httpRange.Length), nil
-	}
 	if f.tmpFile == nil {
-		_, err := f.CacheFullInTempFile()
-		if err != nil {
-			return nil, err
+		if httpRange.Start == 0 && httpRange.Length <= InMemoryBufMaxSizeBytes && f.peekBuff == nil {
+			bufSize := utils.Min(httpRange.Length, f.GetSize())
+			newBuf := bytes.NewBuffer(make([]byte, 0, bufSize))
+			n, err := io.CopyN(newBuf, f.Reader, bufSize)
+			if err != nil {
+				return nil, err
+			}
+			if n != bufSize {
+				return nil, fmt.Errorf("stream RangeRead did not get all data in peek, expect =%d ,actual =%d", bufSize, n)
+			}
+			f.peekBuff = bytes.NewReader(newBuf.Bytes())
+			f.Reader = io.MultiReader(f.peekBuff, f.Reader)
+			return io.NewSectionReader(f.peekBuff, httpRange.Start, httpRange.Length), nil
+		} else {
+			_, err := f.CacheFullInTempFile()
+			if err != nil {
+				return nil, err
+			}
 		}
 	}
 	return io.NewSectionReader(f.tmpFile, httpRange.Start, httpRange.Length), nil
@@ -228,54 +227,7 @@ func (ss *SeekableStream) CacheFullInTempFile() (model.File, error) {
 	return ss.tmpFile, nil
 }
 
-//func (f *FileStream) SetReader(r io.Reader) {
-//	f.Reader = r
-//}
-
-/*
-// RangePeek allow once peek at start of the data, since most drives check first XX bytes for rapid-upload
-func (f *FileStream) RangePeek(length int64) (*bytes.Buffer, error) {
-	if length > InMemoryBufMaxSize*1024*1024 {
-		return nil, errs.NewErr(errs.StreamPeekFail, "can't peek size > %d MB", InMemoryBufMaxSize)
-	}
-	httpRange := &http_range.Range{Length: length}
-	bufSize := utils.Min(httpRange.Length, f.GetSize())
-	buf := bytes.NewBuffer(make([]byte, 0, bufSize))
-	if f.link == nil && f.tmpFile == nil {
-		if !f.peekedOnce {
-			f.mu.Lock()
-			f.peekedOnce = true
-			_, err := io.CopyN(buf, f.Reader, bufSize)
-
-			if err != nil {
-				f.mu.Unlock()
-				return nil, errs.NewErr(errs.StreamPeekFail, "failed to copyN %d bytes data", bufSize)
-			}
-			f.Reader = io.MultiReader(buf, f.Reader)
-			f.mu.Unlock()
-			return buf, nil
-
-		}
-		return nil, errs.NewErr(errs.StreamPeekFail, "link and tmpFile both are null")
-	}
-	f.mu.Lock()
-	defer f.mu.Unlock()
-	rc, _, err := GetReadCloserFromLink(f.Obj, f.link, httpRange)
-
-	if err != nil {
-		return nil, err
-	}
-	_, err = io.CopyN(buf, rc, bufSize)
-	if err != nil {
-		return nil, err
-	}
-	return buf, nil
-}*/
-
-//func (f *FileStream) SetTmpFile(r *os.File) {
-//	f.mu.Lock()
-//	//f.readDisabled = true
-//	f.tmpFile = r
-//	f.Reader = r
-//	f.mu.Unlock()
-//}
+func (f *FileStream) SetTmpFile(r *os.File) {
+	f.Reader = r
+	f.tmpFile = r
+}
diff --git a/server/handles/fsup.go b/server/handles/fsup.go
index c4061be4017..8d925575ed3 100644
--- a/server/handles/fsup.go
+++ b/server/handles/fsup.go
@@ -1,8 +1,13 @@
 package handles
 
 import (
+	"github.com/alist-org/alist/v3/internal/conf"
+	"github.com/alist-org/alist/v3/pkg/utils/random"
+	"io"
 	"net/url"
+	"os"
 	stdpath "path"
+	"reflect"
 	"strconv"
 	"time"
 
@@ -97,7 +102,24 @@ func FsForm(c *gin.Context) {
 		common.ErrorResp(c, err, 500)
 		return
 	}
-	f, err := file.Open()
+	tmpFile, tmpInSys := "", ""
+	fv := reflect.ValueOf(*file)
+	tmpInSys = fv.FieldByName("tmpfile").String()
+
+	var f io.Reader
+	var osFile *os.File
+	if len(tmpInSys) > 0 {
+		tmpFile = conf.Conf.TempDir + "file-" + random.String(8)
+		err = os.Rename(tmpInSys, tmpFile)
+		if err != nil {
+			common.ErrorResp(c, err, 500)
+			return
+		}
+		osFile, err = os.Open(tmpFile)
+		f = osFile
+	} else {
+		f, err = file.Open()
+	}
 	if err != nil {
 		common.ErrorResp(c, err, 500)
 		return
@@ -118,12 +140,16 @@ func FsForm(c *gin.Context) {
 		common.ErrorResp(c, err, 500)
 		return
 	}
+	if osFile != nil {
+		ss.SetTmpFile(osFile)
+	}
+
 	if asTask {
 		err = fs.PutAsTask(dir, ss)
 	} else {
 		err = fs.PutDirectly(c, dir, ss, true)
 	}
-	defer f.Close()
+
 	if err != nil {
 		common.ErrorResp(c, err, 500)
 		return


Test patch: