Repository: AlistGo/alist (language: go)

Issue:
IPFS API 驱动，下载包含空格的文件名时会报错
### 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.30.0

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

IPFS API

### Describe the bug / 问题描述

使用IPFS API 驱动，下载包含空格的文件名时会报错

### Reproduction / 复现链接

不需要链接，本地运行一个ipfs daemon，上传一个文件，并将文件名修改成带空格的文件名就可以

### Config / 配置

{
  "force": false,
  "site_url": "",
  "cdn": "",
  "jwt_secret": "***************",
  "token_expires_in": 48,
  "database": {
    "type": "sqlite3",
    "host": "",
    "port": 0,
    "user": "",
    "password": "",
    "name": "",
    "db_file": "data/data.db",
    "table_prefix": "x_",
    "ssl_mode": ""
  },
  "scheme": {
    "address": "0.0.0.0",
    "http_port": 5244,
    "https_port": -1,
    "force_https": false,
    "cert_file": "",
    "key_file": "",
    "unix_file": "",
    "unix_file_perm": ""
  },
  "temp_dir": "data/temp",
  "bleve_dir": "data/bleve",
  "dist_dir": "",
  "log": {
    "enable": true,
    "name": "data/log/log.log",
    "max_size": 50,
    "max_backups": 30,
    "max_age": 28,
    "compress": false
  },
  "delayed_start": 0,
  "max_connections": 0,
  "tls_insecure_skip_verify": true,
  "tasks": {
    "download": {
      "workers": 5,
      "max_retry": 1
    },
    "transfer": {
      "workers": 5,
      "max_retry": 2
    },
    "upload": {
      "workers": 5,
      "max_retry": 0
    },
    "copy": {
      "workers": 5,
      "max_retry": 2
    }
  },
  "cors": {
    "allow_origins": [
      "*"
    ],
    "allow_methods": [
      "*"
    ],
    "allow_headers": [
      "*"
    ]
  }
}

### Logs / 日志

{"code":500,"message":"http request [http://*********:5080/ipfs/*******************************/?filename=test -HDR.ts] failure,status: 400 response:400 Bad Request","data":null}


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

我来修复这个问题，可能是没有对特殊文件名进行转码。
我这里无法复现这个问题
https://alist.ipfsscan.io/%E8%BD%AF%E4%BB%B6/PotPlayer
> 我这里无法复现这个问题 https://alist.ipfsscan.io/%E8%BD%AF%E4%BB%B6/PotPlayer

我重新测试了一下，需要打开alist的“Web代理”功能才会产生这个问题。
> > 我这里无法复现这个问题 https://alist.ipfsscan.io/%E8%BD%AF%E4%BB%B6/PotPlayer
> 
> 我重新测试了一下，需要打开alist的“Web代理”功能才会产生这个问题。

我开启了web代理，也无法复现。你用我的网关试一下呢  https://cdn.ipfsscan.io
> 我开启了web代理，也无法复现。你用我的网关试一下呢 https://cdn.ipfsscan.io

我发现使用你的网关不会出现这个问题，使用本地kubo跑的网关，就会有这个报错，我猜测是因为cloudflare或者其他HTTP服务给url进行编码了。
另外，我现在发现使用你提供的这个下载案例 https://alist.ipfsscan.io/%E8%BD%AF%E4%BB%B6/PotPlayer 也出现了这个错误。

## curl 结果
``` bash
$ curl -v https://alist.ipfsscan.io/p/%E8%BD%AF%E4%BB%B6/PotPlayer/PotPlayerSetup64%201.7.22150.0.exe?sign=Hqh3n5iyICkiHXaEAOqtgvLS9IuYUb4W8yuccBsnaCw=:0
* Host alist.ipfsscan.io:443 was resolved.
* IPv6: (none)
* IPv4: 104.26.2.186, 104.26.3.186
*   Trying 104.26.2.186:443...
* Connected to alist.ipfsscan.io (104.26.2.186) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=ipfsscan.io
*  start date: Feb 20 12:44:13 2024 GMT
*  expire date: May 20 12:44:12 2024 GMT
*  subjectAltName: host "alist.ipfsscan.io" matched cert's "*.ipfsscan.io"
*  issuer: C=US; O=Google Trust Services LLC; CN=GTS CA 1P5
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (4096/152 Bits/secBits), signed using sha384WithRSAEncryption
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://alist.ipfsscan.io/p/%E8%BD%AF%E4%BB%B6/PotPlayer/PotPlayerSetup64%201.7.22150.0.exe?sign=Hqh3n5iyICkiHXaEAOqtgvLS9IuYUb4W8yuccBsnaCw=:0
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: alist.ipfsscan.io]
* [HTTP/2] [1] [:path: /p/%E8%BD%AF%E4%BB%B6/PotPlayer/PotPlayerSetup64%201.7.22150.0.exe?sign=Hqh3n5iyICkiHXaEAOqtgvLS9IuYUb4W8yuccBsnaCw=:0]
* [HTTP/2] [1] [user-agent: curl/8.6.0]
* [HTTP/2] [1] [accept: */*]
> GET /p/%E8%BD%AF%E4%BB%B6/PotPlayer/PotPlayerSetup64%201.7.22150.0.exe?sign=Hqh3n5iyICkiHXaEAOqtgvLS9IuYUb4W8yuccBsnaCw=:0 HTTP/2
> Host: alist.ipfsscan.io
> User-Agent: curl/8.6.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 200
< date: Wed, 20 Mar 2024 16:28:38 GMT
< content-type: application/json; charset=utf-8
< content-length: 217
< last-modified: Wed, 20 Mar 2024 16:21:32 GMT
< cf-cache-status: HIT
< age: 415
< accept-ranges: bytes
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=CH3r22sfjQb4MwXcKlzuP3OI1ROY7IDfYBTlI%2FdOztkZDet%2B%2FvYhPYROVdRkIqBf9lYuQp43%2FwRGH3KodEmgk%2FFnSB5yLjJJlLLLLvb7SKMYvW1chO%2BjFsrBi8wnBmZz%2B3UU"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< strict-transport-security: max-age=31536000; includeSubDomains; preload
< x-content-type-options: nosniff
< server: cloudflare
< cf-ray: 8677139489922421-IAD
< alt-svc: h3=":443"; ma=86400
<
* Connection #0 to host alist.ipfsscan.io left intact
{"code":500,"message":"http request [http://***********48:8080/ipfs/QmUBberA2c7W36o7sDyzyjQRP6JLvCXNWSr5v7fnwV1At3/?filename=PotPlayerSetup64 1.7.22150.0.exe] failure,status: 400 response:400 Bad Request","data":null}
```

## 截图
![screenshot](https://github.com/alist-org/alist/assets/31086033/d6f4b3ae-9ca3-4321-9e4f-450904c34cec)

Fix patch:
diff --git a/drivers/ipfs_api/driver.go b/drivers/ipfs_api/driver.go
index cf21e62da59..f6f81305e20 100644
--- a/drivers/ipfs_api/driver.go
+++ b/drivers/ipfs_api/driver.go
@@ -62,7 +62,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
 	for _, file := range dirs {
 		gateurl := *d.gateURL
 		gateurl.Path = "ipfs/" + file.Hash
-		gateurl.RawQuery = "filename=" + file.Name
+		gateurl.RawQuery = "filename=" + url.PathEscape(file.Name)
 		objlist = append(objlist, &model.ObjectURL{
 			Object: model.Object{ID: file.Hash, Name: file.Name, Size: int64(file.Size), IsFolder: file.Type == 1},
 			Url:    model.Url{Url: gateurl.String()},
@@ -73,7 +73,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
 }
 
 func (d *IPFS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
-	link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + file.GetName()
+	link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + url.PathEscape(file.GetName())
 	return &model.Link{URL: link}, nil
 }
 


Test patch: