Compare commits

...
Author SHA1 Message Date
Levi 76bec67ab2 ci: fix windows runner 2025-10-12 22:45:43 +08:00
Levi a2a9e3dc2a release: v1.8.2 2025-10-12 21:17:40 +08:00
Levi 9a5680fb45 feat: refactor browser extension communication to use IPC (#1130) 2025-10-12 21:17:07 +08:00
Levi 75a8a3a9ac ci: downgrade to Ubuntu 22.04 (#1128) 2025-10-09 21:01:06 +08:00
Levi 3e8c85b945 feat: update GitHub mirrors (#1137) 2025-10-09 20:59:56 +08:00
Levi 90f70d1bf6 feat: isolate TLS fingerprint for each extension lifecycle (#1135) 2025-10-09 20:37:21 +08:00
Levi 20a70d7f91 fix: certain special single-file torrent downloads (#1136) 2025-09-29 22:52:06 +08:00
Levi d33546a4b5 fix: update deleteAll method to accept force parameter (#1127) 2025-09-29 19:41:48 +08:00
Levi 93614d46a8 release: v1.8.1 2025-09-14 10:10:30 +08:00
Levi 04b0e691eb fix: add timeout to server shutdown (#1116) 2025-09-13 21:19:28 +08:00
铺盖崽 7f08fa0b23 ci: update linux runner 24.04 (#1117) 2025-09-13 18:44:00 +08:00
Levi df6a51d23c feat: add notify when new version option (#1115) 2025-09-12 18:32:45 +08:00
Levi 46c379d556 fix: resolve incorrect download path for single file torrent (#1111) 2025-09-10 07:59:10 +08:00
Levi 46cd9f7a3e fix: windows tray disappearing after sleep/wake cycles (#1108) 2025-09-09 22:44:09 +08:00
Levi 5c2f66f4b6 fix: web auth config (#1094) 2025-09-09 18:35:04 +08:00
Krau cfdcb586ee fix: decode URL-encoded filename from contentDisposition header (#1099) 2025-09-04 18:22:30 +08:00
Levi 55e6bdc4f7 release: v1.8.0 2025-08-28 18:43:44 +08:00
Levi b31b42301e feat: update storage directory path for Windows (#1087) 2025-08-28 08:44:18 +08:00
Levi 7ce0d876b0 feat: add environment variables configuration support (#1088) 2025-08-28 08:21:02 +08:00
Levi 2fd2596aa0 ci: update macOS version to macos-14 (#1086) 2025-08-26 10:07:33 +08:00
Local-Gay-Communist e0973e4a34 feat: add german language support (#1084) 2025-08-22 09:26:37 +08:00
42 changed files with 2161 additions and 269 deletions

No files matched your search

+3 -9
View File
@@ -34,7 +34,7 @@ jobs:
build-windows:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
runs-on: windows-latest
runs-on: windows-2022
needs: [get-release]
steps:
- uses: actions/checkout@v3
@@ -265,12 +265,7 @@ jobs:
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- if: matrix.os == 'ubuntu-22.04'
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- if: matrix.os == 'ubuntu-22.04-arm'
uses: subosito/flutter-action@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: master
@@ -282,7 +277,6 @@ jobs:
sudo apt install -y libfuse2 locate
arch=$(uname -m)
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
chmod +x appimagetool
sudo mv appimagetool /usr/local/bin/
- name: Build
@@ -508,7 +502,7 @@ jobs:
overwrite: true
build-ios:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
runs-on: macos-latest
runs-on: macos-14
needs: [get-release]
steps:
- uses: actions/checkout@v3
+13 -7
View File
@@ -53,7 +53,13 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}
- name: Run Unit tests.
run: go test -v -coverpkg=$(go list ./... | grep -v /cmd/ | grep -v /bind/ | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $(go list ./... | grep -v /cmd/ | grep -v /bind/)
run: |
# Get packages for testing and coverage (including cmd/web for flags_test.go)
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
# Filter out main.go from coverage report to avoid lowering coverage percentage
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
mv coverage_filtered.txt coverage.txt
- uses: codecov/codecov-action@v4
with:
files: ./coverage.txt
@@ -63,8 +69,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
needs: [ test ]
os: [windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
@@ -96,8 +102,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-latest, ubuntu-latest ]
needs: [ test ]
os: [macos-14, ubuntu-latest]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
@@ -129,7 +135,7 @@ jobs:
build-web:
name: Build web check
runs-on: ubuntu-latest
needs: [ test ]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
@@ -149,7 +155,7 @@ jobs:
build-docker:
name: Build docker check
runs-on: ubuntu-latest
needs: [ test ]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
+1
View File
@@ -2,6 +2,7 @@ package main
import (
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
+19
View File
@@ -0,0 +1,19 @@
//go:build !windows
// +build !windows
package main
import (
"net"
"os"
"path/filepath"
)
func Dial() (net.Conn, error) {
// Get binary path
exe, err := os.Executable()
if err != nil {
return nil, err
}
return net.Dial("unix", filepath.Join(filepath.Dir(exe), "gopeed_host.sock"))
}
+11
View File
@@ -0,0 +1,11 @@
package main
import (
"net"
"github.com/Microsoft/go-winio"
)
func Dial() (net.Conn, error) {
return winio.DialPipe(`\\.\pipe\gopeed_host`, nil)
}
+69 -24
View File
@@ -1,22 +1,24 @@
package main
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/pkg/browser"
"github.com/shirou/gopsutil/v4/process"
)
const identifier = "gopeed"
type Message struct {
Method string `json:"method"`
Meta map[string]any `json:"meta"`
Params json.RawMessage `json:"params"`
}
@@ -26,31 +28,74 @@ type Response struct {
Message string `json:"message,omitempty"`
}
var apiMap = map[string]func(params json.RawMessage) (data any, err error){
"ping": func(params json.RawMessage) (data any, err error) {
processes, err := process.Processes()
if err != nil {
return false, err
}
func check() (data bool, err error) {
conn, err := Dial()
if err != nil {
return false, err
}
defer conn.Close()
return true, nil
}
for _, p := range processes {
name, err := p.Name()
if err != nil {
continue
}
func wakeup(hidden bool) error {
running, _ := check()
if running {
return nil
}
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) {
return true, nil
}
uri := "gopeed:"
if hidden {
uri = uri + "?hidden=true"
}
if err := browser.OpenURL(uri); err != nil {
return err
}
for i := 0; i < 10; i++ {
if ok, _ := check(); ok {
return nil
}
return false, nil
time.Sleep(1 * time.Second)
}
return fmt.Errorf("start gopeed failed")
}
var apiMap = map[string]func(message *Message) (data any, err error){
"ping": func(message *Message) (data any, err error) {
return check()
},
"create": func(params json.RawMessage) (data any, err error) {
var strParams string
if err = json.Unmarshal(params, &strParams); err != nil {
"create": func(message *Message) (data any, err error) {
buf, err := message.Params.MarshalJSON()
if err != nil {
return
}
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
silent := false
if v, ok := message.Meta["silent"]; ok {
silent, _ = v.(bool)
}
if err := wakeup(silent); err != nil {
return nil, err
}
client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return Dial()
},
},
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("POST", "http://127.0.0.1/create", bytes.NewBuffer(buf))
if err != nil {
return
}
if message.Meta != nil {
metaJson, _ := json.Marshal(message.Meta)
req.Header.Set("X-Gopeed-Host-Meta", string(metaJson))
}
_, err = client.Do(req)
return
},
}
@@ -88,7 +133,7 @@ func main() {
var data any
var err error
if handler, ok := apiMap[message.Method]; ok {
data, err = handler(message.Params)
data, err = handler(&message)
} else {
err = errors.New("Unknown method: " + message.Method)
}
+5 -6
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"syscall"
"time"
@@ -19,6 +18,7 @@ func main() {
pid := flag.Int("pid", 0, "PID of the process to update")
updateChannel := flag.String("channel", "", "Update channel")
packagePath := flag.String("asset", "", "Path to the package asset")
exeDir := flag.String("exeDir", "", "Directory of the entry executable")
logPath := flag.String("log", "", "Log file path")
flag.Parse()
@@ -43,7 +43,7 @@ func main() {
restart bool
err error
)
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil {
if restart, err = update(*pid, *updateChannel, *packagePath, *exeDir); err != nil {
log.Printf("Update failed: %v\n", err)
os.Exit(1)
}
@@ -61,7 +61,7 @@ func main() {
os.Exit(0)
}
func update(pid int, updateChannel, packagePath string) (restart bool, err error) {
func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, err error) {
killSignalChan := make(chan any, 1)
go func() {
@@ -76,9 +76,8 @@ func update(pid int, updateChannel, packagePath string) (restart bool, err error
}
}()
appDir := filepath.Dir(os.Args[0])
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir)
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
log.Printf("Updating process updateChannel=%s packagePath=%s exeDir=%s\n", updateChannel, packagePath, exeDir)
if restart, err = install(killSignalChan, updateChannel, packagePath, exeDir); err != nil {
return false, errors.Wrap(err, "failed to install package")
}
+9 -5
View File
@@ -25,16 +25,20 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
mountPoint := ""
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, "/Volumes/") {
fields := strings.Fields(line)
if len(fields) > 2 {
mountPoint = fields[len(fields)-1]
// Find the /Volumes/ path in the line
// hdiutil output format: /dev/disk4s1 Apple_HFS /Volumes/Gopeed
// or with sequence number: /dev/disk4s1 Apple_HFS /Volumes/Gopeed 1
idx := strings.Index(line, "/Volumes/")
if idx != -1 {
// Extract everything from /Volumes/ onwards and trim whitespace
mountPoint = strings.TrimSpace(line[idx:])
break
}
}
}
if mountPoint == "" {
return fmt.Errorf("failed to get mount point")
return fmt.Errorf("failed to get mount point from hdiutil output: %s", string(output))
}
// Detach the mounted DMG
@@ -45,7 +49,7 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
return err
}
if len(matches) == 0 {
return fmt.Errorf("no .app found in dmg")
return fmt.Errorf("no .app found in dmg, mountPoint: %s", mountPoint)
}
killSignalChan <- nil
+166 -51
View File
@@ -3,19 +3,23 @@ package main
import (
"encoding/json"
"flag"
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/GopeedLab/gopeed/pkg/base"
)
type args struct {
Address *string `json:"address"`
Port *int `json:"port"`
Username *string `json:"username"`
Password *string `json:"password"`
ApiToken *string `json:"apiToken"`
StorageDir *string `json:"storageDir"`
Address *string `json:"address"`
Port *int `json:"port"`
Username *string `json:"username"`
Password *string `json:"password"`
ApiToken *string `json:"apiToken"`
StorageDir *string `json:"storageDir"`
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
// DownloadConfig when the first time to start the server, it will be configured as initial value
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
@@ -23,62 +27,173 @@ type args struct {
}
func parse() *args {
var cliArgs args
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
cliArgs.Username = flag.String("u", "gopeed", "Web Authentication Username")
cliArgs.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
cfg := &args{}
cliConfig := loadCliArgs()
loadConfigFile(cfg, *cliConfig.configPath)
loadEnvVars(cfg)
// override with non-default command line arguments
overrideWithCliArgs(cfg, cliConfig)
// set default values
setDefaults(cfg, cliConfig)
return cfg
}
// loadCliArgs parses command line arguments and returns initial config
func loadCliArgs() *args {
cfg := &args{}
cfg.Address = flag.String("A", "0.0.0.0", "Bind Address")
cfg.Port = flag.Int("P", 9999, "Bind Port")
cfg.Username = flag.String("u", "gopeed", "Web Authentication Username")
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
cfg.StorageDir = flag.String("d", "", "Storage directory")
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
cfg.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse()
// args priority: config file > cli args
cfgArgs := loadConfig(*cliArgs.configPath)
if cfgArgs.Address == nil {
cfgArgs.Address = cliArgs.Address
// Parse white download directories from comma-separated string
if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
dirs := strings.Split(*whiteDownloadDirs, ",")
for i := range dirs {
dirs[i] = strings.TrimSpace(dirs[i])
}
cfg.WhiteDownloadDirs = dirs
}
if cfgArgs.Port == nil {
cfgArgs.Port = cliArgs.Port
}
if cfgArgs.Username == nil {
cfgArgs.Username = cliArgs.Username
}
if cfgArgs.Password == nil {
cfgArgs.Password = cliArgs.Password
}
if cfgArgs.ApiToken == nil {
cfgArgs.ApiToken = cliArgs.ApiToken
}
if cfgArgs.StorageDir == nil {
cfgArgs.StorageDir = cliArgs.StorageDir
}
return cfgArgs
return cfg
}
func loadConfig(path string) *args {
var args args
// overrideWithCliArgs overrides config with non-empty command line arguments
func overrideWithCliArgs(cfg *args, cliConfig *args) {
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "A":
cfg.Address = cliConfig.Address
case "P":
cfg.Port = cliConfig.Port
case "u":
cfg.Username = cliConfig.Username
case "p":
cfg.Password = cliConfig.Password
case "T":
cfg.ApiToken = cliConfig.ApiToken
case "d":
cfg.StorageDir = cliConfig.StorageDir
case "w":
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
case "c":
cfg.configPath = cliConfig.configPath
}
})
}
if !filepath.IsAbs(path) {
// setDefaults sets default values for any unset configuration fields
func setDefaults(cfg *args, cliConfig *args) {
if cfg.Address == nil {
cfg.Address = cliConfig.Address
}
if cfg.Port == nil {
cfg.Port = cliConfig.Port
}
if cfg.Username == nil {
cfg.Username = cliConfig.Username
}
if cfg.Password == nil {
cfg.Password = cliConfig.Password
}
if cfg.ApiToken == nil {
cfg.ApiToken = cliConfig.ApiToken
}
if cfg.StorageDir == nil {
cfg.StorageDir = cliConfig.StorageDir
}
}
// loadConfigFile loads configuration from file
func loadConfigFile(cfg *args, configPath string) {
if !filepath.IsAbs(configPath) {
dir, err := os.Getwd()
if err != nil {
fmt.Println("config dir get failed, reason:" + err.Error())
return &args
return
}
path = filepath.Join(dir, path)
configPath = filepath.Join(dir, configPath)
}
file, err := os.ReadFile(path)
file, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
return &args
return
}
fmt.Println("config file read failed, reason:" + err.Error())
return &args
return
}
if err = json.Unmarshal(file, &args); err != nil {
fmt.Println("config file parse failed, reason:" + err.Error())
return &args
if err = json.Unmarshal(file, cfg); err != nil {
return
}
}
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
func loadEnvVars(cfg *args) {
v := reflect.ValueOf(cfg).Elem()
t := reflect.TypeOf(cfg).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
// Get json tag as environment variable suffix
jsonTag := fieldType.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Remove options like omitempty
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
jsonTag = jsonTag[:commaIdx]
}
// Convert to uppercase and add GOPEED_ prefix
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
envValue := os.Getenv(envKey)
if envValue == "" {
continue
}
// Set value based on field type
if field.Kind() == reflect.Ptr {
if field.IsNil() {
// Create new pointer instance
newVal := reflect.New(field.Type().Elem())
field.Set(newVal)
}
switch field.Type().Elem().Kind() {
case reflect.String:
field.Elem().SetString(envValue)
case reflect.Int:
if intVal, err := strconv.Atoi(envValue); err == nil {
field.Elem().SetInt(int64(intVal))
}
default:
// For complex types like DownloadConfig, try JSON unmarshaling
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
var config base.DownloaderStoreConfig
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
field.Set(reflect.ValueOf(&config))
}
}
}
} else if field.Kind() == reflect.Slice {
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
if field.Type().Elem().Kind() == reflect.String {
dirs := strings.Split(envValue, ",")
for i := range dirs {
dirs[i] = strings.TrimSpace(dirs[i])
}
field.Set(reflect.ValueOf(dirs))
}
}
}
fmt.Printf("config file loaded: %s\n", path)
return &args
}
File diff suppressed because it is too large. Load diff
+17 -15
View File
@@ -6,11 +6,12 @@ package main
import (
"embed"
"fmt"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs"
"os"
"path/filepath"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
//go:embed dist/*
@@ -31,28 +32,29 @@ func main() {
}
}
var dir string
var storageDir string
if args.StorageDir != nil && *args.StorageDir != "" {
dir = *args.StorageDir
storageDir = *args.StorageDir
} else {
exe, err := os.Executable()
if err != nil {
panic(err)
}
dir = filepath.Dir(exe)
storageDir = filepath.Join(filepath.Dir(exe), "storage")
}
cfg := &model.StartConfig{
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"),
ApiToken: *args.ApiToken,
DownloadConfig: args.DownloadConfig,
ProductionMode: true,
WebEnable: true,
WebFS: sub,
WebAuth: webAuth,
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
StorageDir: storageDir,
WhiteDownloadDirs: args.WhiteDownloadDirs,
ApiToken: *args.ApiToken,
DownloadConfig: args.DownloadConfig,
ProductionMode: true,
WebEnable: true,
WebFS: sub,
WebAuth: webAuth,
}
cmd.Start(cfg)
}
+6 -5
View File
@@ -3,17 +3,18 @@ package bt
import (
"encoding/base64"
"encoding/json"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
gohttp "net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
)
func TestFetcher_Resolve_Torrent(t *testing.T) {
+3 -2
View File
@@ -5,11 +5,12 @@ package bt
import (
"context"
"fmt"
"github.com/anacrolix/torrent/storage"
"io"
"os"
"path/filepath"
"github.com/anacrolix/torrent/storage"
"github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent/common"
@@ -34,7 +35,7 @@ func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
if opts.FilePathMaker == nil {
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
var parts []string
if opts.Info.Name != metainfo.NoName {
if opts.Info.Length == 0 || opts.File.Path == nil {
parts = append(parts, opts.Info.Name)
}
return filepath.Join(append(parts, opts.File.Path...)...)
+12 -10
View File
@@ -23,6 +23,7 @@ import (
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/xiaoqidun/setft"
"golang.org/x/sync/errgroup"
)
@@ -172,13 +173,13 @@ func (f *Fetcher) Resolve(req *base.Request) error {
filename = strings.Replace(filename, "UTF8", "UTF-8", 1)
file.Name, _ = decoder.Decode(filename)
} else {
file.Name = filename
file.Name = util.TryUrlQueryUnescape(filename)
}
} else {
substr := "attachment; filename="
index := strings.Index(contentDisposition, substr)
if index != -1 {
file.Name = contentDisposition[index+len(substr):]
file.Name = util.TryUrlQueryUnescape(contentDisposition[index+len(substr):])
}
}
}
@@ -385,19 +386,20 @@ func (f *Fetcher) run(index int, ctx context.Context) (err error) {
resp *http.Response
)
f.redirectLock.Lock()
if f.redirectURL != "" {
redirectURL := f.redirectURL
if redirectURL != "" {
f.redirectLock.Unlock()
}
err = func() (err error) {
} else {
// Only hold the lock when we need to potentially set redirectURL
defer func() {
if f.redirectURL == "" {
if err == nil {
f.redirectURL = resp.Request.URL.String()
}
f.redirectLock.Unlock()
if redirectURL == "" && err == nil {
f.redirectURL = resp.Request.URL.String()
}
f.redirectLock.Unlock()
}()
}
err = func() (err error) {
httpReq, err = f.buildRequest(ctx, f.meta.Req)
if err != nil {
return
+15 -14
View File
@@ -2,15 +2,6 @@ package download
import (
"errors"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"math"
gohttp "net/http"
"net/url"
@@ -22,6 +13,16 @@ import (
"sync"
"sync/atomic"
"time"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
const (
@@ -532,7 +533,7 @@ func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
if filter == nil || filter.IsEmpty() {
return d.deleteAll()
return d.deleteAll(force)
}
deleteTasks := d.GetTasksByFilter(filter)
@@ -577,7 +578,7 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
return
}
func (d *Downloader) deleteAll() (err error) {
func (d *Downloader) deleteAll(force bool) (err error) {
var deleteTasksTemp []*Task
func() {
d.lock.Lock()
@@ -591,7 +592,7 @@ func (d *Downloader) deleteAll() (err error) {
}()
for _, task := range deleteTasksTemp {
if err = d.doDelete(task, true); err != nil {
if err = d.doDelete(task, force); err != nil {
return
}
}
@@ -941,9 +942,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
}
// if enable white download directory, check if the download directory is in the white list
if len(d.cfg.DownloadDirWhiteList) > 0 {
if len(d.cfg.WhiteDownloadDirs) > 0 {
inWhiteList := false
for _, dir := range d.cfg.DownloadDirWhiteList {
for _, dir := range d.cfg.WhiteDownloadDirs {
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
inWhiteList = true
break
+1 -1
View File
@@ -91,7 +91,7 @@ func TestDownloader_CreateNotInWhite(t *testing.T) {
defer listener.Close()
downloader := NewDownloader(&DownloaderConfig{
DownloadDirWhiteList: []string{"./downloads"},
WhiteDownloadDirs: []string{"./downloads"},
})
if err := downloader.Setup(); err != nil {
t.Fatal(err)
+6 -4
View File
@@ -131,6 +131,7 @@ type XMLHttpRequest struct {
responseHeaders http.Header
aborted bool
client *req.Client
fingerprint string
WithCredentials bool `json:"withCredentials"`
Upload *XMLHttpRequestUpload `json:"upload"`
@@ -164,7 +165,7 @@ func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
}
func (xhr *XMLHttpRequest) Send(data goja.Value) {
setFingerprint(xhr.client)
setFingerprint(xhr.client, xhr.fingerprint)
d := xhr.parseData(data)
var (
@@ -381,7 +382,8 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
}
instance := &XMLHttpRequest{
client: client,
client: client,
fingerprint: util.SafeGet[string](runtime, FingerprintMagicKey),
Upload: &XMLHttpRequestUpload{
EventProp: &EventProp{
eventListeners: make(map[string]func(event *ProgressEvent)),
@@ -395,8 +397,8 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
runtime.Set("__gopeed_setFingerprint", func(fingerprint Fingerprint) {
currentFingerprint = fingerprint
runtime.Set("__gopeed_setFingerprint", func(fingerprint string) {
runtime.Set(FingerprintMagicKey, fingerprint)
})
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
return err
@@ -1,21 +1,19 @@
package xhr
import (
"github.com/imroc/req/v3"
)
import "github.com/imroc/req/v3"
type Fingerprint string
const (
fingerprintChrome Fingerprint = "chrome"
fingerprintFirefox Fingerprint = "firefox"
fingerprintSafari Fingerprint = "safari"
FingerprintMagicKey = "__gopeed_xhr_fingerprint"
fingerprintChrome = "chrome"
fingerprintFirefox = "firefox"
fingerprintSafari = "safari"
)
var currentFingerprint Fingerprint = ""
func setFingerprint(client *req.Client) {
switch currentFingerprint {
func setFingerprint(client *req.Client, fingerprint string) {
switch fingerprint {
case fingerprintChrome:
client.ImpersonateChrome()
case fingerprintFirefox:
+13
View File
@@ -22,3 +22,16 @@ func AssertError[T error](err error) (t T, r bool) {
}
return
}
func SafeGet[T any](vm *goja.Runtime, name string) T {
v := vm.Get(name)
if v == nil {
var init T
return init
}
if e, ok := v.Export().(T); ok {
return e
}
var init T
return init
}
+4 -4
View File
@@ -137,10 +137,10 @@ type DownloaderConfig struct {
Controller *controller.Controller
FetchManagers []fetcher.FetcherManager
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
Storage Storage
StorageDir string
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
RefreshInterval int // RefreshInterval time duration to refresh task progress(ms)
Storage Storage
StorageDir string
WhiteDownloadDirs []string
ProductionMode bool
+7 -4
View File
@@ -65,7 +65,9 @@ func Stop() {
}()
if srv != nil {
if err := srv.Shutdown(context.TODO()); err != nil {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
}
}
@@ -83,8 +85,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
startCfg.Init()
downloadCfg := &download.DownloaderConfig{
ProductionMode: startCfg.ProductionMode,
RefreshInterval: startCfg.RefreshInterval,
ProductionMode: startCfg.ProductionMode,
RefreshInterval: startCfg.RefreshInterval,
WhiteDownloadDirs: startCfg.WhiteDownloadDirs,
}
if startCfg.Storage == model.StorageBolt {
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
@@ -190,7 +193,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
}
if enableBasicAuth {
if r.URL.Path == "/api/web/login" {
if !strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api/web/login" {
h.ServeHTTP(w, r)
return
}
+10
View File
@@ -56,3 +56,13 @@ func ProxyUrlToHandler(proxyUrl *url.URL) func(*http.Request) (*url.URL, error)
}
return http.ProxyURL(proxyUrl)
}
// TryUrlQueryUnescape tries to unescape a URL-encoded string.
//
// If unescaping fails, it returns the original string.
func TryUrlQueryUnescape(s string) string {
if decoded, err := url.QueryUnescape(s); err == nil {
return decoded
}
return s
}
+21
View File
@@ -128,3 +128,24 @@ func TestParseDataUri(t *testing.T) {
})
}
}
func TestTryURLDecode(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"normal.txt", "normal.txt"},
{"%E7%8F%80%E5%B0%94%E8%AF%BA.zip", "珀尔诺.zip"},
{"hello%20world.txt", "hello world.txt"},
{"bad%2-text", "bad%2-text"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := TryUrlQueryUnescape(tt.input)
if got != tt.expected {
t.Errorf("TryUrlQueryUnescape(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}
@@ -81,6 +81,7 @@ class ExtraConfig {
bool lastDeleteTaskKeep;
bool defaultDirectDownload;
bool defaultBtClient;
bool notifyWhenNewVersion;
ExtraConfigBt bt = ExtraConfigBt();
@@ -90,6 +91,7 @@ class ExtraConfig {
this.lastDeleteTaskKeep = false,
this.defaultDirectDownload = false,
this.defaultBtClient = true,
this.notifyWhenNewVersion = true,
});
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
@@ -74,6 +74,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
@@ -83,6 +84,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
'defaultDirectDownload': instance.defaultDirectDownload,
'defaultBtClient': instance.defaultBtClient,
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
'bt': instance.bt.toJson(),
};
@@ -16,6 +16,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart';
import '../../../../api/model/create_task.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../api/model/request.dart';
import '../../../../core/common/start_config.dart';
@@ -31,7 +32,7 @@ import '../../../../util/package_info.dart';
import '../../../../util/updater.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
import '../../create/dto/create_router_params.dart';
import '../../../rpc/rpc.dart';
import '../../redirect/views/redirect_view.dart';
const unixSocketPath = 'gopeed.sock';
@@ -78,6 +79,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
_initTray().onError(
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
_initRpcServer().onError((error, stackTrace) =>
logger.w("initRpcServer error", error, stackTrace));
_initForegroundTask().onError((error, stackTrace) =>
logger.w("initForegroundTask error", error, stackTrace));
@@ -119,7 +123,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
@override
void onTrayIconRightMouseDown() {
trayManager.popUpContextMenu();
trayManager.popUpContextMenu(bringAppToFront: true);
}
@override
@@ -271,6 +275,37 @@ class AppController extends GetxController with WindowListener, TrayListener {
trayManager.addListener(this);
}
Future<void> _initRpcServer() async {
if (!Util.isDesktop()) {
return;
}
try {
await startRpcServer({
"/create": (ctx) async {
final meta =
ctx.request.headers["X-Gopeed-Host-Meta"]?.firstOrNull ?? "{}";
final jsonMeta = jsonDecode(meta);
final silent = jsonMeta['silent'] as bool? ?? false;
final params = await ctx.readText();
final createTaskParams = _decodeToCreatTaskParams(params);
if (!silent) {
await windowManager.show();
_handleToCreate0(createTaskParams);
} else {
try {
await createTask(createTaskParams);
} catch (e) {
logger.w(
"create task from extension fail", e, StackTrace.current);
}
}
},
});
} catch (e) {
logger.w("start rpc server fail", e, StackTrace.current);
}
}
Future<void> _initForegroundTask() async {
if (!Util.isMobile()) {
return;
@@ -317,13 +352,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
if (uri.path == "/create") {
final params = uri.queryParameters["params"];
if (params?.isNotEmpty == true) {
final safeParams = params!.replaceAll(" ", "+");
final paramsJson =
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE,
arguments:
CreateRouterParams.fromJson(jsonDecode(paramsJson))));
_handleToCreate(params!);
return;
}
Get.rootDelegate.offAndToNamed(Routes.CREATE);
@@ -346,7 +375,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE,
arguments: CreateRouterParams(req: Request(url: path))));
arguments: CreateTask(req: Request(url: path))));
}
String runningAddress() {
@@ -506,6 +535,11 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
Future<void> _initCheckUpdate() async {
// Check if auto check update is enabled
if (!downloaderConfig.value.extra.notifyWhenNewVersion) {
return;
}
final versionInfo = await checkUpdate();
if (versionInfo != null) {
await showUpdateDialog(Get.context!, versionInfo);
@@ -519,4 +553,21 @@ class AppController extends GetxController with WindowListener, TrayListener {
apiToken: startConfig.value.apiToken));
await putConfig(downloaderConfig.value);
}
CreateTask _decodeToCreatTaskParams(String params) {
final safeParams = params.replaceAll('"', "").replaceAll(" ", "+");
final paramsJson =
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
return CreateTask.fromJson(jsonDecode(paramsJson));
}
_handleToCreate(String params) {
final createTaskParams = _decodeToCreatTaskParams(params);
_handleToCreate0(createTaskParams);
}
_handleToCreate0(CreateTask createTaskParams) {
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE, arguments: createTaskParams));
}
}
@@ -1,24 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../../api/model/options.dart';
import '../../../../api/model/request.dart';
part 'create_router_params.g.dart';
@JsonSerializable(explicitToJson: true)
class CreateRouterParams {
Request? req;
Options? opt;
CreateRouterParams({
this.req,
this.opt,
});
factory CreateRouterParams.fromJson(
Map<String, dynamic> json,
) =>
_$CreateRouterParamsFromJson(json);
Map<String, dynamic> toJson() => _$CreateRouterParamsToJson(this);
}
@@ -1,31 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'create_router_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CreateRouterParams _$CreateRouterParamsFromJson(Map<String, dynamic> json) =>
CreateRouterParams(
req: json['req'] == null
? null
: Request.fromJson(json['req'] as Map<String, dynamic>),
opt: json['opt'] == null
? null
: Options.fromJson(json['opt'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CreateRouterParamsToJson(CreateRouterParams instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('req', instance.req?.toJson());
writeNotNull('opt', instance.opt?.toJson());
return val;
}
@@ -27,7 +27,6 @@ import '../../../views/file_tree_view.dart';
import '../../app/controllers/app_controller.dart';
import '../../history/views/history_view.dart';
import '../controllers/create_controller.dart';
import '../dto/create_router_params.dart';
class CreateView extends GetView<CreateController> {
final _confirmFormKey = GlobalKey<FormState>();
@@ -76,7 +75,7 @@ class CreateView extends GetView<CreateController> {
_pathController.text = appController.downloaderConfig.value.downloadDir;
}
final CreateRouterParams? routerParams = Get.rootDelegate.arguments();
final CreateTask? routerParams = Get.rootDelegate.arguments();
if (routerParams?.req?.url.isNotEmpty ?? false) {
// get url from route arguments
final url = routerParams!.req!.url;
@@ -531,6 +531,25 @@ class SettingView extends GetView<SettingController> {
);
}
final buildAutoCheckUpdate = _buildConfigItem(
'notifyWhenNewVersion',
() =>
downloaderCfg.value.extra.notifyWhenNewVersion ? 'on'.tr : 'off'.tr,
(Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value: downloaderCfg.value.extra.notifyWhenNewVersion,
onChanged: (bool value) async {
downloaderCfg.update((val) {
val!.extra.notifyWhenNewVersion = value;
});
await debounceSave();
},
),
);
});
buildThanks() {
const thankPage =
'https://github.com/GopeedLab/gopeed/graphs/contributors';
@@ -957,6 +976,7 @@ class SettingView extends GetView<SettingController> {
children: _addDivider([
buildHomepage(),
buildVersion(),
buildAutoCheckUpdate(),
buildThanks(),
]),
)),
+133
View File
@@ -0,0 +1,133 @@
import 'dart:io';
import 'dart:convert';
import 'package:dart_ipc/dart_ipc.dart';
import 'package:path_provider/path_provider.dart';
import '../../util/util.dart';
/// RPC context class that contains request information and response methods
class RpcContext {
final HttpRequest request;
final HttpResponse response;
String? _bodyCache;
RpcContext(this.request, this.response);
/// Read and parse JSON request body
Future<Map<String, dynamic>> readJSON() async {
if (_bodyCache == null) {
final content = <int>[];
await for (var data in request) {
content.addAll(data);
}
_bodyCache = String.fromCharCodes(content);
}
if (_bodyCache!.isEmpty) {
return {};
}
return jsonDecode(_bodyCache!) as Map<String, dynamic>;
}
Future<String> readText() async {
if (_bodyCache == null) {
final content = <int>[];
await for (var data in request) {
content.addAll(data);
}
_bodyCache = String.fromCharCodes(content);
}
return _bodyCache ?? '';
}
/// Write JSON response
Future<void> writeJSON(Map<String, dynamic> data) async {
response.headers.contentType = ContentType.json;
response.write(jsonEncode(data));
await response.close();
}
/// Write error response
Future<void> writeError(String message, [int statusCode = 500]) async {
response.statusCode = statusCode;
await writeJSON({'error': message});
}
}
/// Route handler type definition
typedef RouteHandler = Future<void> Function(RpcContext ctx);
/// Route registry
class RouteRegistry {
final Map<String, RouteHandler> _routes = {};
/// Register a route
void register(String path, RouteHandler handler) {
_routes[path] = handler;
}
/// Get route handler
RouteHandler? getHandler(String path) {
return _routes[path];
}
/// Get all registered routes
List<String> get routes => _routes.keys.toList();
}
/// Start RPC server
Future<void> startRpcServer([Map<String, RouteHandler>? routes]) async {
String path;
if (Util.isWindows()) {
path = r'\\.\pipe\gopeed_host';
} else {
path = await Util.homePathJoin("gopeed_host.sock");
// try to delete existing socket file
final socketFile = File(path);
if (await socketFile.exists()) {
try {
await socketFile.delete();
} catch (e) {
// ignore
}
}
}
// Create route registry
final registry = RouteRegistry();
// Register provided routes
if (routes != null) {
routes.forEach((path, handler) {
registry.register(path, handler);
});
}
final serverSocket = await bind(path);
final httpServer = HttpServer.listenOn(serverSocket);
httpServer.forEach((HttpRequest request) async {
final ctx = RpcContext(request, request.response);
try {
// Get request path
final requestPath = request.uri.path;
// Find route handler
final handler = registry.getHandler(requestPath);
if (handler != null) {
// Execute route handler
await handler(ctx);
} else {
// 404 Route not found
await ctx.writeError('Route not found: $requestPath', 404);
}
} catch (e) {
await ctx.writeError('Internal server error: $e', 500);
} finally {
await request.response.close();
}
});
}
+129
View File
@@ -0,0 +1,129 @@
const deDE = {
'de_DE': {
'label': 'Deutsch',
'error': 'Fehler',
'tip': 'Tipp',
'confirm': 'Bestätigen',
'cancel': 'Abbrechen',
'on': 'An',
'off': 'Aus',
'selectAll': 'Alle Auswählen',
'select': 'Auswählen',
'task': 'Aufgaben',
'downloading': 'Wird Heruntergeladen',
'downloaded': 'Heruntergeladen',
'setting': 'Einstellungen',
'donate': 'Spenden',
'exit': 'Beenden',
'create': 'Aufgabe Erstellen',
'directDownload': 'Direkter Download',
'advancedOptions': 'Erweiterte Einstellungen',
'followSettings': 'Einstellungen Folgen',
'downloadLink': 'Download Link',
'downloadLinkValid': 'Bitte geben Sie den Download-Link ein',
'downloadLinkHit': 'Bitte geben Sie den Download-Link ein, einen pro Zeile@append',
'downloadLinkHitDesktop': ', oder ziehen Sie die Torrent-Datei direkt hierher',
'download': 'Download',
'noFileSelected': 'Bitte wählen Sie mindestens eine Datei aus, um fortzufahren.',
'noStoragePermission': 'Speicherberechtigung erforderlich',
'selectFile': 'Datei auswählen',
'rename': 'Umbenennen',
'basic': 'Basic',
'advanced': 'Fortschrittlich',
'general': 'Allgemein',
'downloadDir': 'Download-Verzeichnis',
'downloadDirValid': 'Bitte wählen Sie das Download-Verzeichnis',
'connections': 'Anschlüsse',
'useServerCtime': 'Serverzeit für die Dateierstellung verwenden',
'maxRunning': 'Maximale Anzahl ausgeführter Aufgaben',
'defaultDirectDownload': 'Direkten Download standardmäßig aktivieren',
'items': '@count Artikel',
'subscribeTracker': 'Abonnement-Tracker',
'subscribeFail':
'Anmeldung fehlgeschlagen. Bitte überprüfen Sie das Netzwerk oder versuchen Sie es später erneut.',
'update': 'Aktualisieren',
'updateDaily': 'Tägliche Aktualisierung',
'lastUpdate': 'Letzte Aktualisierung: @time',
'addTracker': 'Tracker hinzufügen',
'addTrackerHit': 'Bitte geben Sie die URL des Tracker-Servers ein, eine pro Zeile',
'ui': 'Benutzeroberfläche',
'theme': 'Thema',
'themeSystem': 'System',
'themeLight': 'Lichtmodus',
'themeDark': 'Dunkler Modus',
'locale': 'Sprache',
'about': 'Über',
'homepage': 'Homepage',
'version': 'Version',
'protocol': 'Protokoll',
'port': 'Port',
'apiToken': 'API Token',
'notSet': 'NS',
'set': 'SET',
'portInUse': 'Port [@port] wird verwendet, bitte ändern Sie den Port',
'effectAfterRestart': 'Wirkung nach Neustart',
'developer': 'Entwickler-in',
'logDirectory': 'Protokollverzeichnis',
'show': 'Zeigen',
'continue': 'Weitermachen',
'pause': 'Pause',
'startAll': 'Alles starten',
'pauseAll': 'Alle pausieren',
'deleteTask': '@count Aufgaben löschen',
'deleteTaskTip': 'Heruntergeladene Dateien behalten',
'delete': 'Löschen',
'newVersionTitle': 'Entdecken Sie die neue Version @version',
'newVersionUpdate': 'Jetzt aktualisieren',
'newVersionLater': 'Später',
'extensions': 'Erweiterungen',
'extensionInstallUrl': 'Installations-URL',
'extensionInstallSuccess': 'Erfolgreich installiert',
'extensionUpdateSuccess': 'Erfolgreich aktualisiert',
'extensionDelete': 'Erweiterung löschen',
'extensionAlreadyLatest': 'Es ist bereits die neueste Version',
'extensionFind': 'Erweiterungen suchen',
'extensionDevelop': 'Erweiterungen entwickeln',
'history': 'Verlauf',
'clearHistory': 'Verlauf löschen',
'noHistoryFound': 'Kein Verlauf gefunden',
'serviceTitle': 'Download-Service',
'serviceText': 'Läuft',
'network': 'Netzwerk',
'proxy': 'Proxy',
'noProxy': 'Kein Proxy',
'systemProxy': 'System-Proxy',
'customProxy': 'Benutzerdefinierter Proxy',
'server': 'Server',
'username': 'Benutzername',
'password': 'Passwort',
'thanks': 'Danke',
'thanksDesc':
'Vielen Dank an alle Mitwirkenden, die beim Aufbau und der Entwicklung der Gopeed-Community geholfen haben!',
'browserExtension': 'Browsererweiterung',
'launchAtStartup': 'Beim Start starten',
'seedConfig': 'Seed-Konfiguration',
'seedKeep': 'Weiter-seeden bis manuell gestoppt',
'seedRatio': 'Seed Verhältniss',
'seedTime': 'Seed-Zeit (Minuten)',
'setAsDefaultBtClient': 'Als Standard-BT-Client festlegen',
'taskDetail': 'Aufgabendetails',
'taskName': 'Aufgabenname',
'taskUrl': 'Aufgaben-URL',
'downloadPath': 'Download-Pfad',
'skipVerifyCert': 'Zertifikatsüberprüfung überspringen',
'name': 'Name',
'size': 'Größe',
'unknown': 'Unbekannt',
'fileSelectedCount': 'Dateien: ',
'fileSelectedSize': 'Größe: ',
'httpHeaderName': 'Header-Name',
'httpHeaderValue': 'Header-Wert',
'login': 'Login',
'username_required': 'Bitte geben Sie Ihren Benutzernamen ein',
'password_required': 'Bitte geben Sie Ihr Passwort ein',
'login_success': 'Anmeldung erfolgreich',
'login_failed': 'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort',
'login_failed_network':
'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihre Netzwerkverbindung',
},
};
+1
View File
@@ -52,6 +52,7 @@ const enUS = {
'themeLight': 'Light',
'themeDark': 'Dark',
'locale': 'Language',
'notifyWhenNewVersion': 'Notify for updates',
'about': 'About',
'homepage': 'Homepage',
'version': 'Version',
+1
View File
@@ -51,6 +51,7 @@ const zhCN = {
'themeLight': '明亮主题',
'themeDark': '暗黑主题',
'locale': '语言',
'notifyWhenNewVersion': '有新版本时提醒',
'about': '关于',
'homepage': '主页',
'version': '版本',
+2
View File
@@ -1,5 +1,6 @@
import 'package:get/get.dart';
import 'langs/de_de.dart';
import 'langs/en_us.dart';
import 'langs/fa_ir.dart';
import 'langs/fr_fr.dart';
@@ -41,5 +42,6 @@ class _Messages extends Translations {
...ukUA,
...huHU,
...ptBR,
...deDE
};
}
+18 -1
View File
@@ -24,6 +24,8 @@ class Args {
bool hidden = false;
Args();
Args.parse(List<String> args) {
final parser = ArgParser();
parser.addFlag(flagHidden);
@@ -33,7 +35,22 @@ class Args {
}
void main(List<String> arguments) async {
final args = Args.parse(arguments);
Args args;
// Parse url scheme arguments, e.g. gopeed:?hidden=true
// TODO: macos open url handle
// TODO: macos updater test
if (arguments.firstOrNull?.startsWith("gopeed:") == true) {
try {
final uri = Uri.parse(arguments.first);
args = Args()..hidden = uri.queryParameters["hidden"] == "true";
} catch (e) {
// ignore
args = Args.parse([]);
}
} else {
args = Args.parse(arguments);
}
await init(args);
onStart();
@@ -26,7 +26,7 @@ const _firefoxNativeHostsKey = r'Software\Mozilla\NativeMessagingHosts';
/// Install host binary for browser extension
Future<void> doInstallHost() async {
final hostPath = Util.homePathJoin(_hostExecName);
final hostPath = await Util.homePathJoin(_hostExecName);
await Util.installAsset('assets/exec/$_hostExecName', hostPath,
executable: true);
}
@@ -62,7 +62,7 @@ Future<bool> doCheckBrowserInstalled(Browser browser) async {
Future<bool> doCheckManifestInstalled(Browser browser) async {
if (await checkBrowserInstalled(browser) == false) return false;
final manifestPath = _getManifestPath(browser);
final manifestPath = await _getManifestPath(browser);
if (manifestPath == null) return false;
if (Platform.isWindows) {
@@ -86,7 +86,7 @@ Future<void> doInstallManifest(Browser browser) async {
if (await checkBrowserInstalled(browser) == false) return;
if (await checkManifestInstalled(browser)) return;
final manifestPath = _getManifestPath(browser)!;
final manifestPath = (await _getManifestPath(browser))!;
final manifestContent = await _getManifestContent(browser);
final manifestDir = path.dirname(manifestPath);
await Directory(manifestDir).create(recursive: true);
@@ -209,11 +209,11 @@ List<String> _getUnixExecutablePaths(Browser browser) {
}
}
String? _getManifestPath(Browser browser) {
Future<String?> _getManifestPath(Browser browser) async {
final manifestName =
browser == Browser.firefox ? '$_hostName.moz.json' : '$_hostName.json';
if (Platform.isWindows) {
return Util.homePathJoin(manifestName);
return await Util.homePathJoin(manifestName);
}
final home =
@@ -259,7 +259,7 @@ Future<bool> _checkWindowsRegistry(String keyPath) async {
}
Future<String> _getManifestContent(Browser browser) async {
final hostPath = Util.homePathJoin(_hostExecName);
final hostPath = await Util.homePathJoin(_hostExecName);
final manifest = {
'name': _hostName,
'description': 'Gopeed browser extension host',
+12 -8
View File
@@ -2,10 +2,12 @@ import 'package:dio/dio.dart';
// List of mirrors for GitHub
const _sourceMirror = [
// ghfast.top: https://ghfast.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
["https://ghfast.top/", r"(.*)"],
// github.moeyy.xyz: https://github.moeyy.xyz/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
["https://github.moeyy.xyz/", r"(.*)"],
// github.tbedu.top: https://github.tbedu.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
["https://github.tbedu.top/", r"(.*)"],
// fastgit.cc: https://fastgit.cc/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
["https://fastgit.cc/", r"(.*)"],
// gitproxy.click: https://gitproxy.click/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
["https://gitproxy.click/", r"(.*)"],
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
[
"https://fastly.jsdelivr.net/gh",
@@ -15,10 +17,12 @@ const _sourceMirror = [
// List of mirrors for GitHub release
const _assertMirror = [
// ghfast.top: https://ghfast.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
["https://ghfast.top/", r"(.*)"],
// github.moeyy.xyz: https://github.moeyy.xyz/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
["https://github.moeyy.xyz/", r"(.*)"],
// github.tbedu.top: https://https://github.tbedu.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
["https://github.tbedu.top/", r"(.*)"],
// fastgit.cc: https://fastgit.cc/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
["https://fastgit.cc/", r"(.*)"],
// gitproxy.click: https://gitproxy.click/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
["https://gitproxy.click/", r"(.*)"],
];
enum MirrorType {
+6 -2
View File
@@ -52,7 +52,7 @@ final _updaterBin = "updater${Util.isWindows() ? ".exe" : ""}";
Future<void> installUpdater() async {
await Util.installAsset(
'assets/exec/$_updaterBin', Util.homePathJoin(_updaterBin),
'assets/exec/$_updaterBin', await Util.homePathJoin(_updaterBin),
executable: true);
}
@@ -346,7 +346,7 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
case Channel.linuxFlathub:
case Channel.linuxSnap:
case Channel.linuxDeb:
final updaterPath = Util.homePathJoin(_updaterBin);
final updaterPath = await Util.homePathJoin(_updaterBin);
// Check the updater binary is exists
if (!await File(updaterPath).exists()) {
await launchUrl(
@@ -363,6 +363,8 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
Update channel
-asset string
Path to the package asset
-exeDir string
Directory of the entry executable
-log string
Log file path
*/
@@ -373,6 +375,8 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
_channel!.name,
"-asset",
newVersionAssetPath,
"-exeDir",
path.dirname(Platform.resolvedExecutable),
"-log",
path.join(logsDir(), "updater.log")
]);
+11 -5
View File
@@ -54,7 +54,8 @@ class Util {
static Future<void> initStorageDir() async {
var storageDir = "";
if (Util.isWindows()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
storageDir =
path.join(File(Platform.resolvedExecutable).parent.path, "storage");
} else if (!Util.isWeb()) {
if (Util.isLinux()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
@@ -156,10 +157,15 @@ class Util {
};
}
static String homePathJoin(String fileName) {
final execPath = Platform.resolvedExecutable;
final execDir = path.dirname(execPath);
return path.join(execDir, fileName);
static Future<String> homePathJoin(String fileName) async {
if (Util.isWindows()) {
final execPath = Platform.resolvedExecutable;
final execDir = path.dirname(execPath);
return path.join(execDir, fileName);
}
final dir = await getApplicationSupportDirectory();
return path.join(dir.path, fileName);
}
static Future<void> installAsset(String assetPath, String targetPath,
+16 -7
View File
@@ -286,6 +286,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dart_ipc:
dependency: "direct main"
description:
name: dart_ipc
sha256: "6cad558cda5304017c1f581df4c96fd4f8e4ee212aae7bfa4357716236faa9ba"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
dart_style:
dependency: transitive
description:
@@ -1252,11 +1260,12 @@ packages:
tray_manager:
dependency: "direct main"
description:
name: tray_manager
sha256: bdc3ac6c36f3d12d871459e4a9822705ce5a1165a17fa837103bc842719bf3f7
url: "https://pub.dev"
source: hosted
version: "0.2.4"
path: "packages/tray_manager"
ref: main
resolved-ref: "91cdbdc12517d0d832aca715542b9d0329739d5d"
url: "https://github.com/monkeyWie/tray_manager.git"
source: git
version: "0.5.1"
typed_data:
dependency: transitive
description:
@@ -1413,10 +1422,10 @@ packages:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.3"
win32:
dependency: transitive
description:
+7 -2
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.7.1+1
version: 1.8.2+1
environment:
sdk: ">=3.0.0 <4.0.0"
@@ -58,7 +58,11 @@ dependencies:
form_builder_validators: ^11.0.0
flutter_foreground_task: ^8.2.0
open_filex: ^4.5.0
tray_manager: ^0.2.4
tray_manager:
git:
url: https://github.com/monkeyWie/tray_manager.git
ref: main
path: packages/tray_manager
lecle_downloads_path_provider: ^0.0.2+8
hive: ^2.2.3
launch_at_startup: ^0.3.1
@@ -78,6 +82,7 @@ dependencies:
url: https://github.com/hui-z/flutter_install_plugin.git
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
flutter_svg: ^2.0.17
dart_ipc: ^1.0.1
dependency_overrides:
permission_handler_windows:
git: