Initial release of bdrclone
This commit is contained in:
390
cmd/bdrclone/main.go
Normal file
390
cmd/bdrclone/main.go
Normal file
@@ -0,0 +1,390 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gitea.dddbg.com/youbin/bdrclone/internal/auth"
|
||||
"gitea.dddbg.com/youbin/bdrclone/internal/baidu"
|
||||
"gitea.dddbg.com/youbin/bdrclone/internal/config"
|
||||
"gitea.dddbg.com/youbin/bdrclone/internal/mount"
|
||||
"gitea.dddbg.com/youbin/bdrclone/internal/serve"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
type application struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
app := &application{}
|
||||
root := app.command()
|
||||
if err := root.ExecuteContext(ctx); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "错误:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *application) command() *cobra.Command {
|
||||
defaultConfig, _ := config.DefaultPath()
|
||||
cmd := &cobra.Command{
|
||||
Use: "bdrclone",
|
||||
Short: "百度网盘命令行和 FUSE 挂载客户端",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Version: version,
|
||||
}
|
||||
cmd.PersistentFlags().StringVar(&a.configPath, "config", defaultConfig, "配置文件路径")
|
||||
cmd.AddCommand(
|
||||
a.configCommand(), a.authCommand(), a.lsCommand(), a.statCommand(), a.catCommand(),
|
||||
a.mkdirCommand(), a.rmCommand(), a.mvCommand(), a.cpCommand(), a.uploadCommand(),
|
||||
a.downloadCommand(), a.quotaCommand(), a.mountCommand(), a.serveCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *application) client() (*baidu.Client, *config.Config, error) {
|
||||
cfg, err := config.Load(a.configPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := cfg.Validate(true); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
client := baidu.New(cfg, baidu.WithTokenSaver(func(token baidu.Token, expiresAt time.Time) error {
|
||||
cfg.AccessToken = token.AccessToken
|
||||
cfg.RefreshToken = token.RefreshToken
|
||||
cfg.ExpiresAt = expiresAt
|
||||
return config.Save(a.configPath, cfg)
|
||||
}))
|
||||
return client, cfg, nil
|
||||
}
|
||||
|
||||
func (a *application) configCommand() *cobra.Command {
|
||||
var clientID, secret, redirect, root, userAgent string
|
||||
var uploadParts int
|
||||
var partSize int64
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "创建或更新百度开放平台配置",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
cfg := &config.Config{}
|
||||
if old, err := config.Load(a.configPath); err == nil {
|
||||
cfg = old
|
||||
}
|
||||
if clientID != "" {
|
||||
cfg.ClientID = clientID
|
||||
}
|
||||
if secret != "" {
|
||||
cfg.ClientSecret = secret
|
||||
}
|
||||
if redirect != "" {
|
||||
cfg.RedirectURI = redirect
|
||||
}
|
||||
if root != "" {
|
||||
cfg.Root = root
|
||||
}
|
||||
if userAgent != "" {
|
||||
cfg.UserAgent = userAgent
|
||||
}
|
||||
if uploadParts != 0 {
|
||||
cfg.UploadParts = uploadParts
|
||||
}
|
||||
if cmd.Flags().Changed("part-size") {
|
||||
cfg.PartSize = partSize
|
||||
}
|
||||
if err := config.Save(a.configPath, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("配置已保存到", a.configPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&clientID, "client-id", "", "百度开放平台 AppKey")
|
||||
cmd.Flags().StringVar(&secret, "client-secret", "", "百度开放平台 SecretKey")
|
||||
cmd.Flags().StringVar(&redirect, "redirect-uri", "", "OAuth 回调地址")
|
||||
cmd.Flags().StringVar(&root, "root", "", "挂载的网盘根路径")
|
||||
cmd.Flags().StringVar(&userAgent, "user-agent", "", "下载 User-Agent")
|
||||
cmd.Flags().IntVar(&uploadParts, "upload-parts", 0, "并发上传分片数 (1-32)")
|
||||
cmd.Flags().Int64Var(&partSize, "part-size", 0, "上传分片字节数,0 表示按会员等级自动")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *application) authCommand() *cobra.Command {
|
||||
var noOpen, oob bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "通过 OAuth 授权百度网盘",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
cfg, err := config.Load(a.configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := baidu.New(cfg, baidu.WithTokenSaver(func(token baidu.Token, expiresAt time.Time) error {
|
||||
cfg.AccessToken, cfg.RefreshToken, cfg.ExpiresAt = token.AccessToken, token.RefreshToken, expiresAt
|
||||
return config.Save(a.configPath, cfg)
|
||||
}))
|
||||
if oob {
|
||||
err = auth.AuthorizeOOB(cmd.Context(), client, !noOpen, cmd.InOrStdin(), cmd.OutOrStdout())
|
||||
} else {
|
||||
err = auth.Authorize(cmd.Context(), client, cfg.RedirectURI, !noOpen)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("授权成功")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&noOpen, "no-open", false, "不自动打开浏览器")
|
||||
cmd.Flags().BoolVar(&oob, "oob", false, "使用百度页面显示授权码,不启动本机回调服务")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *application) lsCommand() *cobra.Command {
|
||||
var asJSON bool
|
||||
cmd := &cobra.Command{Use: "ls [远端目录]", Args: cobra.MaximumNArgs(1), Short: "列出远端目录", RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := "/"
|
||||
if len(args) > 0 {
|
||||
name = args[0]
|
||||
}
|
||||
entries, err := client.List(cmd.Context(), name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if asJSON {
|
||||
return printJSON(entries)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
kind := "-"
|
||||
if entry.IsDirectory() {
|
||||
kind = "d"
|
||||
}
|
||||
fmt.Printf("%s %12d %s %s\n", kind, entry.Size, entry.ModTime().Format("2006-01-02 15:04:05"), entry.Name())
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
cmd.Flags().BoolVar(&asJSON, "json", false, "输出 JSON")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *application) statCommand() *cobra.Command {
|
||||
return &cobra.Command{Use: "stat <远端路径>", Args: cobra.ExactArgs(1), Short: "查看远端文件元数据", RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry, err := client.Stat(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(entry)
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *application) catCommand() *cobra.Command {
|
||||
return &cobra.Command{Use: "cat <远端文件>", Args: cobra.ExactArgs(1), Short: "输出远端文件", RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry, err := client.Stat(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := client.Open(cmd.Context(), entry, 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
_, err = io.Copy(os.Stdout, body)
|
||||
return err
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *application) mkdirCommand() *cobra.Command {
|
||||
return &cobra.Command{Use: "mkdir <远端目录>", Args: cobra.ExactArgs(1), Short: "创建远端目录", RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.Mkdir(cmd.Context(), args[0])
|
||||
return err
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *application) rmCommand() *cobra.Command {
|
||||
var recursive bool
|
||||
cmd := &cobra.Command{Use: "rm <远端路径>", Args: cobra.ExactArgs(1), Short: "删除远端文件或目录", RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry, err := client.Stat(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDirectory() && !recursive {
|
||||
return errors.New("target is a directory; pass --recursive to delete it")
|
||||
}
|
||||
return client.Delete(cmd.Context(), args[0])
|
||||
}}
|
||||
cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "递归删除目录")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *application) mvCommand() *cobra.Command {
|
||||
return a.manageCommand("mv", "移动或重命名远端路径", func(ctx context.Context, c *baidu.Client, from, to string) error { return c.Move(ctx, from, to) })
|
||||
}
|
||||
func (a *application) cpCommand() *cobra.Command {
|
||||
return a.manageCommand("cp", "复制远端路径", func(ctx context.Context, c *baidu.Client, from, to string) error { return c.Copy(ctx, from, to) })
|
||||
}
|
||||
|
||||
func (a *application) manageCommand(use, short string, fn func(context.Context, *baidu.Client, string, string) error) *cobra.Command {
|
||||
return &cobra.Command{Use: use + " <源路径> <目标路径>", Args: cobra.ExactArgs(2), Short: short, RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(cmd.Context(), client, args[0], args[1])
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *application) uploadCommand() *cobra.Command {
|
||||
return &cobra.Command{Use: "upload <本地文件> <远端文件>", Args: cobra.ExactArgs(2), Short: "分片上传本地文件", RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.UploadFile(cmd.Context(), args[0], args[1], func(done, total int64) {
|
||||
fmt.Fprintf(os.Stderr, "\r上传 %d/%d bytes (%d%%)", done, total, done*100/total)
|
||||
})
|
||||
if err == nil {
|
||||
fmt.Fprintln(os.Stderr)
|
||||
}
|
||||
return err
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *application) downloadCommand() *cobra.Command {
|
||||
return &cobra.Command{Use: "download <远端文件> <本地文件>", Args: cobra.ExactArgs(2), Short: "下载远端文件", RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry, err := client.Stat(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := client.Open(cmd.Context(), entry, 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(args[1]), ".bdrclone-download-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
defer os.Remove(tmpName)
|
||||
_, copyErr := io.Copy(tmp, body)
|
||||
closeErr := tmp.Close()
|
||||
if err := errors.Join(copyErr, closeErr); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpName, args[1])
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *application) quotaCommand() *cobra.Command {
|
||||
return &cobra.Command{Use: "quota", Short: "查看网盘容量", RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
quota, err := client.Quota(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
percent := int64(0)
|
||||
if quota.Total > 0 {
|
||||
percent = quota.Used * 100 / quota.Total
|
||||
}
|
||||
fmt.Printf("已用 %s / 总计 %s (%d%%)\n", formatBytes(quota.Used), formatBytes(quota.Total), percent)
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *application) mountCommand() *cobra.Command {
|
||||
var readOnly bool
|
||||
cmd := &cobra.Command{Use: "mount <挂载点>", Args: cobra.ExactArgs(1), Short: "通过 FUSE 挂载百度网盘", RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(args[0], 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("正在挂载", args[0], ",按 Ctrl-C 卸载")
|
||||
return mount.Mount(cmd.Context(), client, args[0], mount.Options{ReadOnly: readOnly})
|
||||
}}
|
||||
cmd.Flags().BoolVar(&readOnly, "read-only", false, "只读挂载")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a *application) serveCommand() *cobra.Command {
|
||||
var address string
|
||||
cmd := &cobra.Command{Use: "serve", Short: "启动只读 HTTP 文件服务", RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
client, _, err := a.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("HTTP 服务监听 http://" + address)
|
||||
err = serve.New(client, address).ListenAndServe(cmd.Context())
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}}
|
||||
cmd.Flags().StringVar(&address, "addr", "127.0.0.1:8080", "监听地址")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func printJSON(value any) error {
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(value)
|
||||
}
|
||||
|
||||
func formatBytes(value int64) string {
|
||||
const unit = int64(1024)
|
||||
if value < unit {
|
||||
return strconv.FormatInt(value, 10) + " B"
|
||||
}
|
||||
div, exp := unit, 0
|
||||
for n := value / unit; n >= unit && exp < 5; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(value)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
Reference in New Issue
Block a user