mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 10:00:27 +00:00
feat: auto rename on duplicate (#76)
This commit is contained in:
1 parent
048554df4d
commit
0515ce6081
8 files changed
+205
-152
No files matched your search
@@ -3,6 +3,7 @@ package fetcher
|
||||
import (
|
||||
"github.com/monkeyWie/gopeed/internal/controller"
|
||||
"github.com/monkeyWie/gopeed/pkg/base"
|
||||
"path"
|
||||
)
|
||||
|
||||
// Fetcher defines the interface for a download protocol.
|
||||
@@ -36,6 +37,14 @@ type FetcherMeta struct {
|
||||
Opts *base.Options `json:"opts"`
|
||||
}
|
||||
|
||||
func (m *FetcherMeta) Filepath(file *base.FileInfo) string {
|
||||
finalName := m.Opts.Name
|
||||
if finalName == "" {
|
||||
finalName = file.Name
|
||||
}
|
||||
return path.Join(m.Opts.Path, m.Res.RootDir, finalName)
|
||||
}
|
||||
|
||||
// FetcherBuilder defines the interface for a fetcher builder.
|
||||
type FetcherBuilder interface {
|
||||
// Schemes returns the schemes supported by the fetcher.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
||||
"github.com/monkeyWie/gopeed/pkg/base"
|
||||
"github.com/monkeyWie/gopeed/pkg/util"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -74,15 +75,16 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
}
|
||||
defer f.torrent.Drop()
|
||||
res := &base.Resource{
|
||||
Name: f.torrent.Name(),
|
||||
Range: true,
|
||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||
Hash: f.torrent.InfoHash().String(),
|
||||
Name: f.torrent.Name(),
|
||||
Range: true,
|
||||
RootDir: f.torrent.Name(),
|
||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||
Hash: f.torrent.InfoHash().String(),
|
||||
}
|
||||
for i, file := range f.torrent.Files() {
|
||||
res.Files[i] = &base.FileInfo{
|
||||
Name: filepath.Base(file.DisplayPath()),
|
||||
Path: util.Dir(file.Path()),
|
||||
Path: util.Dir(path.Join(f.torrent.Info().Name, file.Path())),
|
||||
Size: file.Length(),
|
||||
}
|
||||
res.Size += file.Length()
|
||||
@@ -101,7 +103,7 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||
}
|
||||
}
|
||||
if opts.Path != "" {
|
||||
f.torrentPaths[f.meta.Res.Hash] = f.meta.Opts.Path
|
||||
f.torrentPaths[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
|
||||
}
|
||||
|
||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||
|
||||
@@ -33,6 +33,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Path: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
||||
"github.com/monkeyWie/gopeed/pkg/base"
|
||||
fhttp "github.com/monkeyWie/gopeed/pkg/protocol/http"
|
||||
"github.com/monkeyWie/gopeed/pkg/util"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"io"
|
||||
"mime"
|
||||
@@ -245,7 +244,7 @@ func (f *Fetcher) Wait() (err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) filepath() string {
|
||||
return util.Filepath(f.meta.Opts.Path, f.meta.Res.Files[0].Name, f.meta.Opts.Name)
|
||||
return f.meta.Filepath(f.meta.Res.Files[0])
|
||||
}
|
||||
|
||||
func (f *Fetcher) fetch() {
|
||||
|
||||
+18
-20
@@ -1,27 +1,25 @@
|
||||
package base
|
||||
|
||||
import "github.com/monkeyWie/gopeed/pkg/util"
|
||||
import (
|
||||
"github.com/monkeyWie/gopeed/pkg/util"
|
||||
)
|
||||
|
||||
// 下载请求
|
||||
// Request download request
|
||||
type Request struct {
|
||||
// 下载链接
|
||||
URL string `json:"url"`
|
||||
// 附加信息
|
||||
Extra any `json:"extra"`
|
||||
URL string `json:"url"`
|
||||
Extra any `json:"extra"`
|
||||
}
|
||||
|
||||
// 资源信息
|
||||
// Resource download resource
|
||||
type Resource struct {
|
||||
// 资源名称
|
||||
Name string `json:"name"`
|
||||
// 资源总大小
|
||||
Size int64 `json:"size"`
|
||||
// 是否支持断点下载
|
||||
Range bool `json:"range"`
|
||||
// 资源所包含的文件列表
|
||||
Size int64 `json:"size"`
|
||||
// is support range download
|
||||
Range bool `json:"range"`
|
||||
RootDir string `json:"rootDir"`
|
||||
// file list
|
||||
Files []*FileInfo `json:"files"`
|
||||
// 资源hash值
|
||||
Hash string `json:"hash"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
@@ -30,15 +28,15 @@ type FileInfo struct {
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// 下载选项
|
||||
// Options for download
|
||||
type Options struct {
|
||||
// 保存文件名
|
||||
// Download file name
|
||||
Name string `json:"name"`
|
||||
// 保存目录
|
||||
// Download file path
|
||||
Path string `json:"path"`
|
||||
// 选择下载的文件下标列表
|
||||
// Select file indexes to download
|
||||
SelectFiles []int `json:"selectFiles"`
|
||||
// 附加信息
|
||||
// Extra info for specific fetcher
|
||||
Extra any `json:"extra"`
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
||||
"github.com/monkeyWie/gopeed/pkg/base"
|
||||
"github.com/monkeyWie/gopeed/pkg/util"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -200,10 +202,12 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
|
||||
if !ok {
|
||||
return "", errors.New("invalid resource id")
|
||||
}
|
||||
delete(d.fetcherMap, rrId)
|
||||
meta := fetcher.Meta()
|
||||
meta.Opts = opts
|
||||
res := meta.Res
|
||||
if len(opts.SelectFiles) == 0 {
|
||||
opts.SelectFiles = make([]int, 0)
|
||||
for i := range fetcher.Meta().Res.Files {
|
||||
for i := range res.Files {
|
||||
opts.SelectFiles = append(opts.SelectFiles, i)
|
||||
}
|
||||
}
|
||||
@@ -221,6 +225,25 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// check if the download file is duplicated and rename it automatically.
|
||||
files := res.Files
|
||||
if len(files) == 1 {
|
||||
fullPath := meta.Filepath(files[0])
|
||||
newName, err := util.CheckDuplicateAndRename(fullPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
opts.Name = newName
|
||||
} else if res.RootDir != "" {
|
||||
fullDirPath := path.Join(opts.Path, res.RootDir)
|
||||
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.RootDir = newName
|
||||
}
|
||||
|
||||
err = fetcher.Create(opts)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -234,7 +257,7 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
|
||||
task.Status = base.DownloadStatusRunning
|
||||
// calculate select files size
|
||||
for _, selectIndex := range opts.SelectFiles {
|
||||
task.Size += fetcher.Meta().Res.Files[selectIndex].Size
|
||||
task.Size += res.Files[selectIndex].Size
|
||||
}
|
||||
initTask(task)
|
||||
task.timer.Start()
|
||||
@@ -252,7 +275,6 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
|
||||
}
|
||||
d.emit(EventKeyStart, task)
|
||||
taskId = task.ID
|
||||
|
||||
go func() {
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
@@ -261,6 +283,7 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
|
||||
}
|
||||
d.watch(task)
|
||||
}()
|
||||
delete(d.fetcherMap, rrId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -336,11 +359,17 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
|
||||
return
|
||||
}
|
||||
if force {
|
||||
names := make([]string, 0)
|
||||
for _, file := range task.Meta.Res.Files {
|
||||
names = append(names, util.Filepath(file.Path, file.Name, task.Meta.Opts.Name))
|
||||
if task.Meta.Res.RootDir != "" {
|
||||
if err = os.RemoveAll(path.Join(task.Meta.Opts.Path, task.Meta.Res.RootDir)); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for _, file := range task.Meta.Res.Files {
|
||||
if err = util.SafeRemove(task.Meta.Filepath(file)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
util.SafeRemoveAll(task.Meta.Opts.Path, names)
|
||||
}
|
||||
d.emit(EventKeyDelete, task)
|
||||
task = nil
|
||||
|
||||
+42
-24
@@ -2,9 +2,11 @@ package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
syspath "path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Dir(path string) string {
|
||||
@@ -30,39 +32,55 @@ func SafeRemove(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SafeRemoveAll remove file and parent directories safely
|
||||
func SafeRemoveAll(path string, names []string) error {
|
||||
for _, name := range names {
|
||||
err := SafeRemove(syspath.Join(path, name))
|
||||
if err != nil {
|
||||
return err
|
||||
// CheckDuplicateAndRename rename duplicate file, add suffix (1) (2) ...
|
||||
// if file name is a.txt, rename to a (1).txt
|
||||
// if directory name is a, rename to a (1)
|
||||
// return new name
|
||||
func CheckDuplicateAndRename(path string) (string, error) {
|
||||
dir := syspath.Dir(path)
|
||||
name := syspath.Base(path)
|
||||
|
||||
// if file not exists, return directly
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return name, nil
|
||||
}
|
||||
if err := safeRemoveParent(path, name); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
index := strings.LastIndex(name, ".")
|
||||
var nameTpl string
|
||||
if index == -1 {
|
||||
nameTpl = name + " (%d)"
|
||||
} else {
|
||||
nameTpl = name[:index] + " (%d)" + name[index:]
|
||||
}
|
||||
for i := 1; ; i++ {
|
||||
newName := fmt.Sprintf(nameTpl, i)
|
||||
newPath := syspath.Join(dir, newName)
|
||||
if _, err := os.Stat(newPath); os.IsNotExist(err) {
|
||||
return newName, nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func safeRemoveParent(path string, subPath string) error {
|
||||
currPath := syspath.Dir(subPath)
|
||||
if currPath == "." {
|
||||
return nil
|
||||
// GetSingleDir get the top level single folder name,if not exist, return empty string
|
||||
func GetSingleDir(paths []string) string {
|
||||
if len(paths) == 0 {
|
||||
return ""
|
||||
}
|
||||
// if directory is empty, remove it
|
||||
dir := syspath.Join(path, syspath.Dir(subPath))
|
||||
empty, err := isEmpty(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
split := strings.Split(paths[0], "/")
|
||||
if len(split) == 0 || split[0] == "" {
|
||||
return ""
|
||||
}
|
||||
if empty {
|
||||
err = SafeRemove(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
dir := split[0]
|
||||
for i := 1; i < len(paths); i++ {
|
||||
if !strings.HasPrefix(paths[i], dir) {
|
||||
return ""
|
||||
}
|
||||
return safeRemoveParent(path, currPath)
|
||||
}
|
||||
return nil
|
||||
return dir
|
||||
}
|
||||
|
||||
// check directory is empty
|
||||
|
||||
+89
-92
@@ -2,7 +2,6 @@ package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -106,99 +105,97 @@ func TestSafeRemove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeRemoveAll(t *testing.T) {
|
||||
testDir := "test_dir"
|
||||
err := os.Mkdir(testDir, 0755)
|
||||
func TestGetSingleDir(t *testing.T) {
|
||||
type args struct {
|
||||
paths []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
paths: []string{},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "blank",
|
||||
args: args{
|
||||
paths: []string{""},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "blank multi",
|
||||
args: args{
|
||||
paths: []string{"", "a"},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "multi dir not single",
|
||||
args: args{
|
||||
paths: []string{"a", "b"},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "single dir",
|
||||
args: args{
|
||||
paths: []string{"a"},
|
||||
},
|
||||
want: "a",
|
||||
},
|
||||
{
|
||||
name: "multi dir single",
|
||||
args: args{
|
||||
paths: []string{"a/b", "a/c"},
|
||||
},
|
||||
want: "a",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetSingleDir(tt.args.paths); got != tt.want {
|
||||
t.Errorf("IsSingleDir() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDuplicateAndRename(t *testing.T) {
|
||||
doCheckDuplicateAndRename(t, []string{}, "a.txt", "a.txt")
|
||||
doCheckDuplicateAndRename(t, []string{"a.txt"}, "a.txt", "a (1).txt")
|
||||
doCheckDuplicateAndRename(t, []string{"a.txt", "a (1).txt"}, "a.txt", "a (2).txt")
|
||||
|
||||
doCheckDuplicateAndRename(t, []string{}, "a", "a")
|
||||
doCheckDuplicateAndRename(t, []string{"a"}, "a", "a (1)")
|
||||
doCheckDuplicateAndRename(t, []string{"a", "a (1)"}, "a", "a (2)")
|
||||
}
|
||||
|
||||
func doCheckDuplicateAndRename(t *testing.T, exitsPaths []string, path string, except string) {
|
||||
for _, path := range exitsPaths {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for _, path := range exitsPaths {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := CheckDuplicateAndRename(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
doSafeRemoveAll(t, testDir, []string{
|
||||
"1.txt",
|
||||
}, []string{})
|
||||
|
||||
doSafeRemoveAll(t, testDir, []string{
|
||||
"1.txt",
|
||||
}, []string{
|
||||
"2.txt",
|
||||
}, "2.txt")
|
||||
|
||||
doSafeRemoveAll(t, testDir, []string{
|
||||
"1.txt",
|
||||
"a/b/c/1.txt",
|
||||
"a/b/1.txt",
|
||||
"a/1.txt",
|
||||
}, []string{})
|
||||
|
||||
doSafeRemoveAll(t, testDir, []string{
|
||||
"1.txt",
|
||||
"a/b/c/1.txt",
|
||||
"a/b/1.txt",
|
||||
"a/1.txt",
|
||||
}, []string{
|
||||
"a/b/2.txt",
|
||||
}, "a/b/2.txt")
|
||||
|
||||
doSafeRemoveAll(t, testDir, []string{
|
||||
"1.txt",
|
||||
"a/b/c/1.txt",
|
||||
"a/b/1.txt",
|
||||
"a/1.txt",
|
||||
}, []string{
|
||||
"a/2.txt",
|
||||
}, "a/2.txt")
|
||||
}
|
||||
|
||||
func doSafeRemoveAll(t *testing.T, path string, downloadNames []string, otherNames []string, exist ...string) {
|
||||
preCreate(t, path, downloadNames)
|
||||
preCreate(t, path, otherNames)
|
||||
|
||||
if err := SafeRemoveAll(path, downloadNames); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(exist) == 0 {
|
||||
for _, name := range downloadNames {
|
||||
filePath := filepath.Join(path, name)
|
||||
if isExist(filePath) {
|
||||
t.Fatalf("file %s should not exist", filePath)
|
||||
}
|
||||
subDirPath := filepath.Dir(name)
|
||||
if subDirPath != "." {
|
||||
dirPath := filepath.Join(path, subDirPath)
|
||||
if isExist(dirPath) {
|
||||
t.Fatalf("dir %s should not exist", dirPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, name := range exist {
|
||||
if !isExist(filepath.Join(path, name)) {
|
||||
t.Fatalf("file %s should exist", name)
|
||||
}
|
||||
if got != except {
|
||||
t.Errorf("CheckDuplicateAndRename() = %v, want %v", got, except)
|
||||
}
|
||||
}
|
||||
|
||||
func preCreate(t *testing.T, path string, names []string) {
|
||||
for _, name := range names {
|
||||
fullPath := filepath.Join(path, name)
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isExist(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil || os.IsExist(err)
|
||||
}
|
||||
Reference in new issue
Block a user