Repository: AlistGo/alist (language: go)

Issue:
地址树支持新增文件
### Please make sure of the following things

- [x] I have read the [documentation](https://alist.nn.ci).
- [x] I'm sure there are no duplicate issues or discussions.
- [x] I'm sure this feature is not implemented.
- [x] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

支持使用/apt/fs/put创建URL TREE文件，方便直接使用页面添加链接。

### Suggested solution / 实现思路

根据 #7779 的实现，仿照PutURL MakeDir 解析 /api/fs/put 提交文件的文件名字段。
文件名输入规范按照目录树文件规范中的一行描述：`显示文件名:文件大小:文件修改时间戳:文件链接`


### Additional context / 附件

_No response_


Discussion / hints:

Fix patch:
diff --git a/drivers/url_tree/driver.go b/drivers/url_tree/driver.go
index f97d5cc5e76..049bd2db63f 100644
--- a/drivers/url_tree/driver.go
+++ b/drivers/url_tree/driver.go
@@ -243,7 +243,25 @@ func (d *Urls) PutURL(ctx context.Context, dstDir model.Obj, name, url string) (
 }
 
 func (d *Urls) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
-	return errs.UploadNotSupported
+	if !d.Writable {
+		return errs.PermissionDenied
+	}
+	d.mutex.Lock()
+	defer d.mutex.Unlock()
+	node := GetNodeFromRootByPath(d.root, dstDir.GetPath()) // parent
+	if node == nil {
+		return errs.ObjectNotFound
+	}
+	if node.isFile() {
+		return errs.NotFolder
+	}
+	file, err := parseFileLine(stream.GetName(), d.HeadSize)
+	if err != nil {
+		return err
+	}
+	node.Children = append(node.Children, file)
+	d.updateStorage()
+	return nil
 }
 
 func (d *Urls) updateStorage() {
diff --git a/internal/op/fs.go b/internal/op/fs.go
index 01727e7598c..dcdeb5ad50b 100644
--- a/internal/op/fs.go
+++ b/internal/op/fs.go
@@ -9,6 +9,7 @@ import (
 	"github.com/alist-org/alist/v3/internal/driver"
 	"github.com/alist-org/alist/v3/internal/errs"
 	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/internal/stream"
 	"github.com/alist-org/alist/v3/pkg/generic_sync"
 	"github.com/alist-org/alist/v3/pkg/singleflight"
 	"github.com/alist-org/alist/v3/pkg/utils"
@@ -510,6 +511,12 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file mod
 			log.Errorf("failed to close file streamer, %v", err)
 		}
 	}()
+	// UrlTree PUT
+	if storage.GetStorage().Driver == "UrlTree" {
+		var link string
+		dstDirPath, link = urlTreeSplitLineFormPath(stdpath.Join(dstDirPath, file.GetName()))
+		file = &stream.FileStream{Obj: &model.Object{Name: link}}
+	}
 	// if file exist and size = 0, delete it
 	dstDirPath = utils.FixAndCleanPath(dstDirPath)
 	dstPath := stdpath.Join(dstDirPath, file.GetName())
diff --git a/internal/op/path.go b/internal/op/path.go
index 27f7e183228..912a00008e1 100644
--- a/internal/op/path.go
+++ b/internal/op/path.go
@@ -2,6 +2,7 @@ package op
 
 import (
 	"github.com/alist-org/alist/v3/internal/errs"
+	stdpath "path"
 	"strings"
 
 	"github.com/alist-org/alist/v3/internal/driver"
@@ -27,3 +28,30 @@ func GetStorageAndActualPath(rawPath string) (storage driver.Driver, actualPath
 	actualPath = utils.FixAndCleanPath(strings.TrimPrefix(rawPath, mountPath))
 	return
 }
+
+// urlTreeSplitLineFormPath 分割path中分割真实路径和UrlTree定义字符串
+func urlTreeSplitLineFormPath(path string) (pp string, file string) {
+	// url.PathUnescape 会移除 // ，手动加回去
+	path = strings.Replace(path, "https:/", "https://", 1)
+	path = strings.Replace(path, "http:/", "http://", 1)
+	if strings.Contains(path, ":https:/") || strings.Contains(path, ":http:/") {
+		// URL-Tree模式 /url_tree_drivr/file_name[:size[:time]]:https://example.com/file
+		fPath := strings.SplitN(path, ":", 2)[0]
+		pp, _ = stdpath.Split(fPath)
+		file = path[len(pp):]
+	} else if strings.Contains(path, "/https:/") || strings.Contains(path, "/http:/") {
+		// URL-Tree模式 /url_tree_drivr/https://example.com/file
+		index := strings.Index(path, "/http://")
+		if index == -1 {
+			index = strings.Index(path, "/https://")
+		}
+		pp = path[:index]
+		file = path[index+1:]
+	} else {
+		pp, file = stdpath.Split(path)
+	}
+	if pp == "" {
+		pp = "/"
+	}
+	return
+}


Test patch: