feat: auto rename on duplicate (#76)

This commit is contained in:
Li Wei authored and GitHub committed 2023-02-10 15:18:31 +08:00
1 parent 048554df4d
commit 0515ce6081
8 files changed
+205 -152

No files matched your search

+9
View File
@@ -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
View File
@@ -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))
+1
View File
@@ -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,
},
},
+1 -2
View File
@@ -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
View File
@@ -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"`
}
+37 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}