153 lines
4.4 KiB
Go
153 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type downloadResult struct {
|
|
Path string `json:"path"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
ContentType string `json:"contentType"`
|
|
}
|
|
|
|
func runDownload(ctx context.Context, args []string, client *apiClient, ioStreams streams) *commandError {
|
|
var code, output string
|
|
var force, help bool
|
|
fs := flag.NewFlagSet("download", flag.ContinueOnError)
|
|
fs.SetOutput(io.Discard)
|
|
fs.StringVar(&code, "C", "", "取件码")
|
|
fs.StringVar(&code, "code", "", "取件码")
|
|
fs.StringVar(&output, "O", "", "输出路径")
|
|
fs.StringVar(&output, "output", "", "输出路径")
|
|
fs.BoolVar(&force, "force", false, "覆盖已有文件")
|
|
fs.BoolVar(&help, "help", false, "显示帮助")
|
|
args = interspersedArgs(args, map[string]bool{"C": true, "code": true, "O": true, "output": true})
|
|
if err := fs.Parse(args); err != nil {
|
|
return usageError(err.Error())
|
|
}
|
|
if help {
|
|
_, _ = io.WriteString(ioStreams.out, downloadHelp)
|
|
return nil
|
|
}
|
|
if fs.NArg() != 1 {
|
|
return usageError("download 需要且只接受一个 8 位文件码;运行 ufile-cli help download")
|
|
}
|
|
hash := strings.TrimSpace(fs.Arg(0))
|
|
if len(hash) != 8 {
|
|
return usageError("文件码必须是 8 位")
|
|
}
|
|
if code != "" && !pickupCodePattern.MatchString(code) {
|
|
return usageError("--code 必须是 6 位数字")
|
|
}
|
|
if output == "" {
|
|
return usageError("必须使用 -O/--output 指定输出路径;使用 - 可写入 stdout")
|
|
}
|
|
if output == "-" && force {
|
|
return usageError("输出到 stdout 时不能使用 --force")
|
|
}
|
|
|
|
var destination io.Writer = ioStreams.out
|
|
var temp *os.File
|
|
var tempName string
|
|
if output != "-" {
|
|
if info, err := os.Stat(output); err == nil && info.IsDir() {
|
|
return localError("目标路径是目录,请使用完整文件路径", nil)
|
|
} else if err == nil && !force {
|
|
return localError("目标文件已存在;如需覆盖请使用 --force", nil)
|
|
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return localError("检查目标文件失败", err)
|
|
}
|
|
var err error
|
|
temp, err = os.CreateTemp(filepath.Dir(output), ".ufile-download-*")
|
|
if err != nil {
|
|
return localError("创建临时下载文件失败", err)
|
|
}
|
|
tempName = temp.Name()
|
|
defer os.Remove(tempName)
|
|
destination = temp
|
|
}
|
|
|
|
query := url.Values{}
|
|
if code != "" {
|
|
query.Set("code", code)
|
|
}
|
|
path := "/uf/" + url.PathEscape(hash)
|
|
if encoded := query.Encode(); encoded != "" {
|
|
path += "?" + encoded
|
|
}
|
|
request, err := client.newRequest(ctx, http.MethodGet, path, nil)
|
|
if err != nil {
|
|
closeTemp(temp)
|
|
return classifyError(err)
|
|
}
|
|
request.Header.Set("Accept", "application/octet-stream")
|
|
response, err := client.http.Do(request)
|
|
if err != nil {
|
|
closeTemp(temp)
|
|
return classifyError(err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
closeTemp(temp)
|
|
return classifyError(decodeAPIError(response))
|
|
}
|
|
|
|
size, copyErr := io.Copy(destination, response.Body)
|
|
if temp != nil {
|
|
closeErr := temp.Close()
|
|
temp = nil
|
|
if copyErr == nil {
|
|
copyErr = closeErr
|
|
}
|
|
}
|
|
if copyErr != nil {
|
|
return localError("写入下载内容失败;本次访问次数可能已经被消耗", copyErr)
|
|
}
|
|
if output == "-" {
|
|
return nil
|
|
}
|
|
if force {
|
|
if err := os.Remove(output); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return localError("覆盖已有文件失败;本次访问次数已经被消耗", err)
|
|
}
|
|
} else if _, err := os.Stat(output); err == nil {
|
|
return localError("下载期间目标文件已被创建,未覆盖该文件;本次访问次数已经被消耗", nil)
|
|
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return localError("检查目标文件失败;本次访问次数已经被消耗", err)
|
|
}
|
|
if err := os.Rename(tempName, output); err != nil {
|
|
return localError("保存下载文件失败;本次访问次数已经被消耗", err)
|
|
}
|
|
|
|
result := downloadResult{
|
|
Path: output, Name: responseFilename(response), Size: size,
|
|
ContentType: response.Header.Get("Content-Type"),
|
|
}
|
|
writeJSON(ioStreams.out, result)
|
|
return nil
|
|
}
|
|
|
|
func responseFilename(response *http.Response) string {
|
|
_, params, err := mime.ParseMediaType(response.Header.Get("Content-Disposition"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return filepath.Base(params["filename"])
|
|
}
|
|
|
|
func closeTemp(file *os.File) {
|
|
if file != nil {
|
|
_ = file.Close()
|
|
}
|
|
}
|