fix: torrent redundant directory (#649)

This commit is contained in:
Levi authored and GitHub committed 2024-08-14 21:54:59 +08:00
1 parent d9f04ae928
commit ece5b91dc2
13 files changed
+300 -92

No files matched your search

+4
View File
@@ -110,6 +110,10 @@ type FetcherManager interface {
Filters() []*SchemeFilter
// Build returns a new fetcher.
Build() Fetcher
// ParseName name displayed when the task is not yet resolved, parsed from the request URL
ParseName(u string) string
// AutoRename returns whether the fetcher need renaming the download file when has the same name file.
AutoRename() bool
// DefaultConfig returns the default configuration of the protocol.
DefaultConfig() any
+30 -5
View File
@@ -11,6 +11,7 @@ import (
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"net/url"
"path/filepath"
"strings"
"sync"
@@ -108,7 +109,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
func (f *Fetcher) Create(opts *base.Options) (err error) {
f.meta.Opts = opts
if f.meta.Res != nil {
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
torrentDirMap[f.meta.Res.Hash] = opts.Path
}
return nil
}
@@ -120,7 +121,7 @@ func (f *Fetcher) Start() (err error) {
}
}
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
ft.setTorrentDir(f.meta.FolderPath())
ft.setTorrentDir(f.meta.Opts.Path)
}
files := f.torrent.Files()
// If the user does not specify the file to download, all files will be downloaded by default
@@ -245,16 +246,18 @@ func (f *Fetcher) isDone() bool {
func (f *Fetcher) updateRes() {
res := &base.Resource{
Name: f.torrent.Name(),
Range: true,
Files: make([]*base.FileInfo, len(f.torrent.Files())),
Hash: f.torrent.InfoHash().String(),
}
f.torrent.PeerConns()
// Directory torrent
if f.torrent.Info().Length == 0 {
res.Name = f.torrent.Name()
}
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(file.DisplayPath()),
Size: file.Length(),
}
}
@@ -458,6 +461,28 @@ func (fm *FetcherManager) Build() fetcher.Fetcher {
return &Fetcher{}
}
func (fm *FetcherManager) ParseName(u string) string {
var name string
url, err := url.Parse(u)
if err != nil {
return ""
}
params := url.Query()
if params.Get("dn") != "" {
return params.Get("dn")
}
if params.Get("xt") != "" {
xt := strings.Split(params.Get("xt"), ":")
return xt[len(xt)-1]
}
return name
}
func (fm *FetcherManager) AutoRename() bool {
return false
}
func (fm *FetcherManager) DefaultConfig() any {
return &config{
ListenPort: 0,
+113 -24
View File
@@ -33,7 +33,6 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
}
want := &base.Resource{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
Range: true,
Files: []*base.FileInfo{
@@ -69,33 +68,123 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
}
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
err := fetcher.Resolve(&base.Request{
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
Extra: bt.ReqExtra{
Trackers: []string{
"udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce",
t.Run("Resolve Single File", func(t *testing.T) {
err := fetcher.Resolve(&base.Request{
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
Extra: bt.ReqExtra{
Trackers: []string{
"udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce",
},
},
},
})
if err != nil {
panic(err)
}
})
if err != nil {
panic(err)
}
want := &base.Resource{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
Range: true,
Files: []*base.FileInfo{
{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
want := &base.Resource{
Size: 1466714112,
Range: true,
Files: []*base.FileInfo{
{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
},
},
},
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
}
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
}
})
t.Run("Resolve Multi Files", func(t *testing.T) {
err := fetcher.Resolve(&base.Request{
URL: "./testdata/test.torrent",
Extra: bt.ReqExtra{
Trackers: []string{
"udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce",
},
},
})
if err != nil {
panic(err)
}
want := &base.Resource{
Name: "test",
Size: 107484864,
Range: true,
Files: []*base.FileInfo{
{
Name: "c.txt",
Path: "path",
Size: 98501754,
},
{
Name: "b.txt",
Size: 8904996,
},
{
Name: "a.txt",
Size: 78114,
},
},
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
}
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
}
})
}
func TestFetcherManager_ParseName(t *testing.T) {
type args struct {
u string
}
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
tests := []struct {
name string
args args
want string
}{
{
name: "broken url",
args: args{
u: "magnet://!@#%github.com",
},
want: "",
},
{
name: "dn",
args: args{
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed&dn=ubuntu-22.04-live-server-amd64.iso",
},
want: "ubuntu-22.04-live-server-amd64.iso",
},
{
name: "no dn",
args: args{
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed",
},
want: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
},
{
name: "non standard magnet",
args: args{
u: "magnet:?xxt=abcd",
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fm := &FetcherManager{}
if got := fm.ParseName(tt.args.u); got != tt.want {
t.Errorf("ParseName() = %v, want %v", got, tt.want)
}
})
}
}
Binary file not shown.
+22 -2
View File
@@ -99,7 +99,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
}
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
// response 206 status code, support breakpoint continuation
res.Range = true
// 解析资源大小: bytes 0-1000/1001 => 1001
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
@@ -111,7 +111,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
res.Size = parse
}
} else if base.HttpCodeOK == httpResp.StatusCode {
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
// response 200 status code, not support breakpoint continuation, get file size by Content-Length header
// if not found, maybe chunked encoding
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
if contentLength != "" {
parse, err := strconv.ParseInt(contentLength, 10, 64)
@@ -481,6 +482,25 @@ func (fm *FetcherManager) Build() fetcher.Fetcher {
return &Fetcher{}
}
func (fm *FetcherManager) ParseName(u string) string {
var name string
url, err := url.Parse(u)
if err != nil {
return ""
}
// Get filePath by URL
name = path.Base(url.Path)
// If file name is empty, use host name
if name == "" || name == "/" || name == "." {
name = url.Hostname()
}
return name
}
func (fm *FetcherManager) AutoRename() bool {
return true
}
func (fm *FetcherManager) DefaultConfig() any {
return &config{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
+51 -3
View File
@@ -192,6 +192,54 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
}
}
func TestFetcherManager_ParseName(t *testing.T) {
type args struct {
u string
}
tests := []struct {
name string
args args
want string
}{
{
name: "broken url",
args: args{
u: "https://!@#%github.com",
},
want: "",
},
{
name: "file path",
args: args{
u: "https://github.com/index.html",
},
want: "index.html",
},
{
name: "file path with query and hash",
args: args{
u: "https://github.com/a/b/index.html/#list?name=1",
},
want: "index.html",
},
{
name: "no file path",
args: args{
u: "https://github.com",
},
want: "github.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fm := &FetcherManager{}
if got := fm.ParseName(tt.args.u); got != tt.want {
t.Errorf("ParseName() = %v, want %v", got, tt.want)
}
})
}
}
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
return doDownloadReady(buildFetcher(), listener, connections, t)
}
@@ -359,11 +407,11 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
}
func buildFetcher() *Fetcher {
fb := new(FetcherManager)
fetcher := fb.Build()
fm := new(FetcherManager)
fetcher := fm.Build()
newController := controller.NewController()
newController.GetConfig = func(v any) {
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
json.Unmarshal([]byte(test.ToJson(fm.DefaultConfig())), v)
}
fetcher.Setup(newController)
return fetcher.(*Fetcher)
+21 -19
View File
@@ -930,26 +930,28 @@ func (d *Downloader) doStart(task *Task) (err error) {
}
if isCreate {
d.checkDuplicateLock.Lock()
defer d.checkDuplicateLock.Unlock()
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
// check if the download file is duplicated and rename it automatically.
if task.Meta.Res.Name != "" {
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
fullDirPath := task.Meta.FolderPath()
newName, err := util.CheckDuplicateAndRename(fullDirPath)
if err != nil {
return err
if task.fetcherManager.AutoRename() {
d.checkDuplicateLock.Lock()
defer d.checkDuplicateLock.Unlock()
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
// check if the download file is duplicated and rename it automatically.
if task.Meta.Res.Name != "" {
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
fullDirPath := task.Meta.FolderPath()
newName, err := util.CheckDuplicateAndRename(fullDirPath)
if err != nil {
return err
}
task.Meta.Opts.Name = newName
} else {
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
fullFilePath := task.Meta.SingleFilepath()
newName, err := util.CheckDuplicateAndRename(fullFilePath)
if err != nil {
return err
}
task.Meta.Opts.Name = newName
}
task.Meta.Opts.Name = newName
} else {
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
fullFilePath := task.Meta.SingleFilepath()
newName, err := util.CheckDuplicateAndRename(fullFilePath)
if err != nil {
return err
}
task.Meta.Opts.Name = newName
}
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
+42
View File
@@ -1,6 +1,7 @@
package download
import (
"encoding/json"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/protocol/bt"
@@ -49,6 +50,47 @@ func NewTask() *Task {
}
}
// Name returns the display name of the task.
func (t *Task) Name() string {
// Custom name first
if t.Meta.Opts.Name != "" {
return t.Meta.Opts.Name
}
// Task is not resolved, parse the name from the URL
if t.Meta.Res == nil {
fallbackName := "unknown"
if t.fetcherManager == nil {
return fallbackName
}
parseName := t.fetcherManager.ParseName(t.Meta.Req.URL)
if parseName == "" {
return fallbackName
}
return parseName
}
// Task is a folder
if t.Meta.Res.Name != "" {
return t.Meta.Res.Name
}
// Get the name of the first file
return t.Meta.Res.Files[0].Name
}
func (t *Task) MarshalJSON() ([]byte, error) {
type rawTaskType Task
jsonTask := struct {
rawTaskType
Name string `json:"name"`
}{
rawTaskType(*t),
t.Name(),
}
return json.Marshal(jsonTask)
}
func (t *Task) updateStatus(status base.Status) {
t.UpdatedAt = time.Now()
t.Status = status
@@ -9,7 +9,7 @@ part of 'downloader_config.dart';
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
DownloaderConfig(
downloadDir: json['downloadDir'] as String? ?? '',
maxRunning: json['maxRunning'] as int? ?? 0,
maxRunning: (json['maxRunning'] as num?)?.toInt() ?? 0,
)
..protocolConfig = ProtocolConfig.fromJson(
json['protocolConfig'] as Map<String, dynamic>?)
@@ -38,7 +38,7 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
userAgent: json['userAgent'] as String? ?? '',
connections: json['connections'] as int? ?? 0,
connections: (json['connections'] as num?)?.toInt() ?? 0,
useServerCtime: json['useServerCtime'] as bool? ?? false,
);
@@ -50,14 +50,14 @@ Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
};
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig(
listenPort: json['listenPort'] as int? ?? 0,
listenPort: (json['listenPort'] as num?)?.toInt() ?? 0,
trackers: (json['trackers'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
seedKeep: json['seedKeep'] as bool? ?? false,
seedRatio: (json['seedRatio'] as num?)?.toDouble() ?? 0,
seedTime: json['seedTime'] as int? ?? 0,
seedTime: (json['seedTime'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
+2
View File
@@ -11,6 +11,7 @@ enum Protocol { http, bt }
@JsonSerializable(explicitToJson: true)
class Task {
String id;
String name;
Protocol? protocol;
Meta meta;
Status status;
@@ -21,6 +22,7 @@ class Task {
Task({
required this.id,
required this.name,
required this.meta,
required this.status,
required this.uploading,
+7 -5
View File
@@ -8,6 +8,7 @@ part of 'task.dart';
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
id: json['id'] as String,
name: json['name'] as String,
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
status: $enumDecode(_$StatusEnumMap, json['status']),
uploading: json['uploading'] as bool,
@@ -19,6 +20,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
Map<String, dynamic> _$TaskToJson(Task instance) {
final val = <String, dynamic>{
'id': instance.id,
'name': instance.name,
};
void writeNotNull(String key, dynamic value) {
@@ -52,11 +54,11 @@ const _$ProtocolEnumMap = {
};
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
used: json['used'] as int,
speed: json['speed'] as int,
downloaded: json['downloaded'] as int,
uploadSpeed: json['uploadSpeed'] as int,
uploaded: json['uploaded'] as int,
used: (json['used'] as num).toInt(),
speed: (json['speed'] as num).toInt(),
downloaded: (json['downloaded'] as num).toInt(),
uploadSpeed: (json['uploadSpeed'] as num).toInt(),
uploaded: (json['uploaded'] as num).toInt(),
);
Map<String, dynamic> _$ProgressToJson(Progress instance) => <String, dynamic>{
@@ -82,8 +82,7 @@ class TaskView extends GetView<TaskController> {
),
ListTile(
title: Text('taskName'.tr),
subtitle:
buildTooltipSubtitle(selectTask.value?.showName)),
subtitle: buildTooltipSubtitle(selectTask.value?.name)),
ListTile(
title: Text('taskUrl'.tr),
subtitle:
@@ -121,37 +120,12 @@ class TaskView extends GetView<TaskController> {
}
extension TaskEnhance on Task {
String get showName {
if (meta.opts.name.isNotEmpty) {
return meta.opts.name;
}
if (meta.res == null) {
final u = Uri.parse(meta.req.url);
if (u.scheme.startsWith("http")) {
return u.path.isNotEmpty
? u.path.substring(u.path.lastIndexOf("/") + 1)
: u.host;
} else {
final params = u.queryParameters;
if (params.containsKey("dn")) {
return params["dn"]!;
} else {
return params["xt"]!.split(":").last;
}
}
}
if (meta.res!.name.isNotEmpty) {
return meta.res!.name;
}
return meta.res!.files[0].name;
}
bool get isFolder {
return meta.res?.name.isNotEmpty ?? false;
}
String get explorerUrl {
return path.join(Util.safeDir(meta.opts.path), Util.safeDir(showName));
return path.join(Util.safeDir(meta.opts.path), Util.safeDir(name));
}
Future<void> explorer() async {
@@ -183,10 +183,10 @@ class BuildTaskListView extends GetView {
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(task.showName),
title: Text(task.name),
leading: isFolderTask()
? const Icon(FaIcons.folder)
: Icon(FaIcons.allIcons[findIcon(task.showName)])),
: Icon(FaIcons.allIcons[findIcon(task.name)])),
Row(
children: [
Expanded(