805 lines
26 KiB
Go
805 lines
26 KiB
Go
|
|
// commands.go - встроенные команды fush shell (стиль busybox)
|
|||
|
|
// Реализует базовые команды: exit, ls, cd, mkdir, rm, touch, pwd, cat, echo, mycurl
|
|||
|
|
// Все команды работают независимо от ОС, используя только стандартную библиотеку Go
|
|||
|
|
|
|||
|
|
package shell
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bufio"
|
|||
|
|
"crypto/tls"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"net/http"
|
|||
|
|
"net/http/cookiejar"
|
|||
|
|
"net/url"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"regexp"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"fush/pkg/ansi"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// cmdExit обрабатывает команду exit
|
|||
|
|
func (s *Shell) cmdExit(args []string) error {
|
|||
|
|
s.running.Store(false)
|
|||
|
|
s.logger.Info("Выполнена команда exit")
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdHelp обрабатывает команду help (обновленная с mycurl)
|
|||
|
|
func (s *Shell) cmdHelp(args []string) error {
|
|||
|
|
fmt.Println()
|
|||
|
|
ansi.Println(ansi.Cyan, "╔══════════════════════════════════════════════════════════════╗")
|
|||
|
|
ansi.Println(ansi.Cyan, "║ fush shell - Доступные команды ║")
|
|||
|
|
ansi.Println(ansi.Cyan, "╚══════════════════════════════════════════════════════════════╝")
|
|||
|
|
fmt.Println()
|
|||
|
|
|
|||
|
|
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|||
|
|
ansi.Println(ansi.BrightGreen, "ВСТРОЕННЫЕ КОМАНДЫ (BUSYBOX-STYLE):")
|
|||
|
|
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|||
|
|
|
|||
|
|
commands := []struct {
|
|||
|
|
name string
|
|||
|
|
desc string
|
|||
|
|
}{
|
|||
|
|
{"exit", "Выход из оболочки"},
|
|||
|
|
{"help", "Показать эту справку"},
|
|||
|
|
{"ls [path]", "Вывести список файлов в директории"},
|
|||
|
|
{"cd [dir]", "Сменить текущую директорию"},
|
|||
|
|
{"pwd", "Показать текущую директорию"},
|
|||
|
|
{"mkdir [-p] <dir>", "Создать новую директорию"},
|
|||
|
|
{"rm [-rf] <file>", "Удалить файл или директорию"},
|
|||
|
|
{"touch <file>", "Создать файл или обновить время доступа"},
|
|||
|
|
{"cat <file>", "Вывести содержимое файла"},
|
|||
|
|
{"echo [text...]", "Вывести текст на экран"},
|
|||
|
|
{"exec <cmd> [args...]", "Выполнить внешнюю команду"},
|
|||
|
|
{"mycurl [опции] <URL>", "Скачать файл (аналог curl)"},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, cmd := range commands {
|
|||
|
|
fmt.Printf(" %-20s %s\n", ansi.Colorize(cmd.name, ansi.BrightWhite), cmd.desc)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fmt.Println()
|
|||
|
|
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|||
|
|
ansi.Println(ansi.BrightGreen, "ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:")
|
|||
|
|
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|||
|
|
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("ls -la", ansi.BrightWhite), "Показать все файлы")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("cd /tmp", ansi.BrightWhite), "Перейти в /tmp")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("pwd", ansi.BrightWhite), "Показать текущий путь")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("mkdir -p a/b/c", ansi.BrightWhite), "Создать вложенные директории")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("rm -rf olddir", ansi.BrightWhite), "Удалить директорию рекурсивно")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("cat file.txt", ansi.BrightWhite), "Показать содержимое файла")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("echo Hello World", ansi.BrightWhite), "Вывести текст")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("exec go version", ansi.BrightWhite), "Выполнить внешнюю команду")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("mycurl -o file.zip 'https://example.com/file.zip'", ansi.BrightWhite), "Скачать файл (URL в кавычках)")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("mycurl -O 'https://example.com/image.jpg'", ansi.BrightWhite), "Скачать с сохранением имени")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("curl -s 'https://example.com/file.dat'", ansi.BrightWhite), "Использовать как curl")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("ls | grep .go", ansi.BrightWhite), "Пайплайн")
|
|||
|
|
fmt.Printf(" %-30s %s\n", ansi.Colorize("ls > files.txt", ansi.BrightWhite), "Перенаправление вывода")
|
|||
|
|
|
|||
|
|
fmt.Println()
|
|||
|
|
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdLs обрабатывает команду ls (стиль busybox)
|
|||
|
|
func (s *Shell) cmdLs(args []string) ([]byte, error) {
|
|||
|
|
path := "."
|
|||
|
|
showAll := false
|
|||
|
|
longFormat := false
|
|||
|
|
|
|||
|
|
// Парсим аргументы
|
|||
|
|
for _, arg := range args {
|
|||
|
|
if arg == "-a" || arg == "--all" {
|
|||
|
|
showAll = true
|
|||
|
|
} else if arg == "-l" {
|
|||
|
|
longFormat = true
|
|||
|
|
} else if !strings.HasPrefix(arg, "-") {
|
|||
|
|
path = arg
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
dir, err := os.Open(path)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
defer dir.Close()
|
|||
|
|
|
|||
|
|
entries, err := dir.Readdir(-1)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var output strings.Builder
|
|||
|
|
|
|||
|
|
for _, entry := range entries {
|
|||
|
|
name := entry.Name()
|
|||
|
|
|
|||
|
|
// Пропускаем скрытые файлы если не указан -a
|
|||
|
|
if !showAll && strings.HasPrefix(name, ".") {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if longFormat {
|
|||
|
|
// Формат: права ссылки владелец группа размер дата имя
|
|||
|
|
perms := entry.Mode().String()
|
|||
|
|
nlink := 1 // В Go сложно получить количество жестких ссылок
|
|||
|
|
uid := fmt.Sprintf("%d", entry.Sys() != nil) // Упрощённо
|
|||
|
|
gid := "users"
|
|||
|
|
size := entry.Size()
|
|||
|
|
modTime := entry.ModTime().Format("Jan _2 15:04")
|
|||
|
|
|
|||
|
|
if entry.IsDir() {
|
|||
|
|
name = name + "/"
|
|||
|
|
} else if entry.Mode()&os.ModeSymlink != 0 {
|
|||
|
|
// Для симлинков пытаемся прочитать цель
|
|||
|
|
if target, err := os.Readlink(filepath.Join(path, entry.Name())); err == nil {
|
|||
|
|
name = name + " -> " + target
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fmt.Fprintf(&output, "%s %3d %-8s %-8s %8d %s %s\n",
|
|||
|
|
perms, nlink, uid, gid, size, modTime, name)
|
|||
|
|
} else {
|
|||
|
|
if entry.IsDir() {
|
|||
|
|
name = name + "/"
|
|||
|
|
}
|
|||
|
|
fmt.Fprint(&output, name, "\n")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return []byte(output.String()), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdCd обрабатывает команду cd
|
|||
|
|
func (s *Shell) cmdCd(args []string) error {
|
|||
|
|
path := s.GetEnv("HOME")
|
|||
|
|
if len(args) > 0 {
|
|||
|
|
path = args[0]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if path == "~" {
|
|||
|
|
path = s.GetEnv("HOME")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := os.Chdir(path); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pwd, err := os.Getwd()
|
|||
|
|
if err == nil {
|
|||
|
|
s.SetEnv("PWD", pwd)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdPwd обрабатывает команду pwd
|
|||
|
|
func (s *Shell) cmdPwd(args []string) ([]byte, error) {
|
|||
|
|
dir, err := os.Getwd()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return []byte(dir + "\n"), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdMkdir обрабатывает команду mkdir (с поддержкой -p)
|
|||
|
|
func (s *Shell) cmdMkdir(args []string) error {
|
|||
|
|
if len(args) == 0 {
|
|||
|
|
return fmt.Errorf("требуется имя директории")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createParents := false
|
|||
|
|
var dirs []string
|
|||
|
|
|
|||
|
|
for _, arg := range args {
|
|||
|
|
if arg == "-p" || arg == "--parents" {
|
|||
|
|
createParents = true
|
|||
|
|
} else if !strings.HasPrefix(arg, "-") {
|
|||
|
|
dirs = append(dirs, arg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, dir := range dirs {
|
|||
|
|
var err error
|
|||
|
|
if createParents {
|
|||
|
|
err = os.MkdirAll(dir, 0755)
|
|||
|
|
} else {
|
|||
|
|
err = os.Mkdir(dir, 0755)
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdRm обрабатывает команду rm (с поддержкой -r и -f)
|
|||
|
|
func (s *Shell) cmdRm(args []string) error {
|
|||
|
|
if len(args) == 0 {
|
|||
|
|
return fmt.Errorf("требуется имя файла")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
recursive := false
|
|||
|
|
force := false
|
|||
|
|
var targets []string
|
|||
|
|
|
|||
|
|
for _, arg := range args {
|
|||
|
|
switch arg {
|
|||
|
|
case "-r", "-R", "--recursive":
|
|||
|
|
recursive = true
|
|||
|
|
case "-f", "--force":
|
|||
|
|
force = true
|
|||
|
|
default:
|
|||
|
|
if !strings.HasPrefix(arg, "-") {
|
|||
|
|
targets = append(targets, arg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, target := range targets {
|
|||
|
|
info, err := os.Stat(target)
|
|||
|
|
if err != nil {
|
|||
|
|
if !force {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if info.IsDir() && !recursive {
|
|||
|
|
return fmt.Errorf("'%s' является директорией, используйте -r для удаления", target)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := os.RemoveAll(target); err != nil && !force {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdTouch обрабатывает команду touch
|
|||
|
|
func (s *Shell) cmdTouch(args []string) error {
|
|||
|
|
if len(args) == 0 {
|
|||
|
|
return fmt.Errorf("требуется имя файла")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
now := time.Now()
|
|||
|
|
|
|||
|
|
for _, filename := range args {
|
|||
|
|
if strings.HasPrefix(filename, "-") {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
file.Close()
|
|||
|
|
|
|||
|
|
if err := os.Chtimes(filename, now, now); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdCat обрабатывает команду cat (конкатенация файлов)
|
|||
|
|
func (s *Shell) cmdCat(args []string) ([]byte, error) {
|
|||
|
|
if len(args) == 0 {
|
|||
|
|
// Читаем из stdin
|
|||
|
|
info, err := os.Stdin.Stat()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if info.Mode()&os.ModeCharDevice != 0 {
|
|||
|
|
return nil, fmt.Errorf("ожидается файл или stdin")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reader := bufio.NewReader(os.Stdin)
|
|||
|
|
var output strings.Builder
|
|||
|
|
for {
|
|||
|
|
line, err := reader.ReadString('\n')
|
|||
|
|
if err == io.EOF {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
output.WriteString(line)
|
|||
|
|
}
|
|||
|
|
return []byte(output.String()), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var output strings.Builder
|
|||
|
|
|
|||
|
|
for _, filename := range args {
|
|||
|
|
if strings.HasPrefix(filename, "-") {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
data, err := os.ReadFile(filename)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("ошибка чтения '%s': %v", filename, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
output.Write(data)
|
|||
|
|
if len(data) > 0 && data[len(data)-1] != '\n' {
|
|||
|
|
output.WriteByte('\n')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return []byte(output.String()), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdEcho обрабатывает команду echo
|
|||
|
|
func (s *Shell) cmdEcho(args []string) ([]byte, error) {
|
|||
|
|
newline := true
|
|||
|
|
startIdx := 0
|
|||
|
|
|
|||
|
|
if len(args) > 0 && args[0] == "-n" {
|
|||
|
|
newline = false
|
|||
|
|
startIdx = 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var output strings.Builder
|
|||
|
|
for i := startIdx; i < len(args); i++ {
|
|||
|
|
if i > startIdx {
|
|||
|
|
output.WriteByte(' ')
|
|||
|
|
}
|
|||
|
|
output.WriteString(args[i])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if newline {
|
|||
|
|
output.WriteByte('\n')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return []byte(output.String()), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// resolveURL преобразует относительный URL в абсолютный
|
|||
|
|
func resolveURL(baseURL, relURL string) (string, error) {
|
|||
|
|
base, err := url.Parse(baseURL)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rel, err := url.Parse(relURL)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resolved := base.ResolveReference(rel)
|
|||
|
|
return resolved.String(), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdMycurl обрабатывает команду mycurl для скачивания файлов
|
|||
|
|
func (s *Shell) cmdMycurl(args []string) error {
|
|||
|
|
if len(args) == 0 {
|
|||
|
|
return fmt.Errorf("использование: mycurl [опции] <URL>\n" +
|
|||
|
|
"Опции:\n" +
|
|||
|
|
" -o <файл> выходной файл (по умолчанию из URL)\n" +
|
|||
|
|
" -O использовать имя файла из URL\n" +
|
|||
|
|
" -L следовать редиректам (включено по умолчанию)\n" +
|
|||
|
|
" -s тихий режим (без прогресс-бара)\n" +
|
|||
|
|
" -v подробный вывод\n" +
|
|||
|
|
" -A <строка> User-Agent строка\n" +
|
|||
|
|
" -e <строка> Referer строка\n" +
|
|||
|
|
" -k игнорировать проверку SSL сертификатов\n" +
|
|||
|
|
" -h, --help показать справку\n\n" +
|
|||
|
|
"Важно: URL должен быть заключён в кавычки, если содержит спецсимволы (&, ?, =)")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Парсим аргументы
|
|||
|
|
outputFile := ""
|
|||
|
|
silent := false
|
|||
|
|
verbose := false
|
|||
|
|
urlStr := ""
|
|||
|
|
userAgent := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
|
|||
|
|
referer := "https://futriix.ru:8083/"
|
|||
|
|
insecure := false
|
|||
|
|
maxRedirects := 10
|
|||
|
|
|
|||
|
|
for i := 0; i < len(args); i++ {
|
|||
|
|
arg := args[i]
|
|||
|
|
switch arg {
|
|||
|
|
case "-o":
|
|||
|
|
if i+1 < len(args) {
|
|||
|
|
outputFile = args[i+1]
|
|||
|
|
i++
|
|||
|
|
} else {
|
|||
|
|
return fmt.Errorf("-o требует имя файла")
|
|||
|
|
}
|
|||
|
|
case "-O":
|
|||
|
|
outputFile = "" // Будет определено из URL позже
|
|||
|
|
case "-L":
|
|||
|
|
// Поддержка редиректов включена по умолчанию, игнорируем
|
|||
|
|
case "-s":
|
|||
|
|
silent = true
|
|||
|
|
case "-v":
|
|||
|
|
verbose = true
|
|||
|
|
case "-k":
|
|||
|
|
insecure = true
|
|||
|
|
case "-A":
|
|||
|
|
if i+1 < len(args) {
|
|||
|
|
userAgent = args[i+1]
|
|||
|
|
i++
|
|||
|
|
} else {
|
|||
|
|
return fmt.Errorf("-A требует строку User-Agent")
|
|||
|
|
}
|
|||
|
|
case "-e":
|
|||
|
|
if i+1 < len(args) {
|
|||
|
|
referer = args[i+1]
|
|||
|
|
i++
|
|||
|
|
} else {
|
|||
|
|
return fmt.Errorf("-e требует строку Referer")
|
|||
|
|
}
|
|||
|
|
case "-h", "--help":
|
|||
|
|
return s.cmdMycurlHelp(args)
|
|||
|
|
default:
|
|||
|
|
if !strings.HasPrefix(arg, "-") && urlStr == "" {
|
|||
|
|
urlStr = arg
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if urlStr == "" {
|
|||
|
|
return fmt.Errorf("URL не указан")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Удаляем кавычки, если они есть (одинарные или двойные)
|
|||
|
|
urlStr = strings.Trim(urlStr, "'\"")
|
|||
|
|
|
|||
|
|
// Декодируем URL если нужно (обработка экранированных символов)
|
|||
|
|
decodedURL, err := url.QueryUnescape(urlStr)
|
|||
|
|
if err == nil && decodedURL != urlStr {
|
|||
|
|
if verbose {
|
|||
|
|
fmt.Printf("Декодированный URL: %s\n", decodedURL)
|
|||
|
|
}
|
|||
|
|
urlStr = decodedURL
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверяем валидность URL
|
|||
|
|
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
|||
|
|
return fmt.Errorf("неверный URL (должен начинаться с http:// или https://): %s", urlStr)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Определяем имя выходного файла
|
|||
|
|
if outputFile == "" {
|
|||
|
|
outputFile = getFilenameFromURL(urlStr)
|
|||
|
|
if outputFile == "" {
|
|||
|
|
outputFile = "downloaded_file"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !silent {
|
|||
|
|
fmt.Printf("Загрузка: %s\n", urlStr)
|
|||
|
|
fmt.Printf("Сохраняется в: %s\n", outputFile)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Создаем Cookie Jar для сохранения сессии
|
|||
|
|
jar, err := cookiejar.New(nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("ошибка создания cookie jar: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Создаем HTTP клиент с поддержкой кук и редиректов
|
|||
|
|
client := &http.Client{
|
|||
|
|
Timeout: 60 * time.Second,
|
|||
|
|
Jar: jar,
|
|||
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|||
|
|
if verbose {
|
|||
|
|
fmt.Printf("-> HTTP редирект на: %s\n", req.URL.String())
|
|||
|
|
}
|
|||
|
|
if len(via) >= maxRedirects {
|
|||
|
|
return fmt.Errorf("слишком много HTTP редиректов")
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Если нужно игнорировать SSL сертификаты
|
|||
|
|
if insecure {
|
|||
|
|
transport := &http.Transport{
|
|||
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|||
|
|
}
|
|||
|
|
client.Transport = transport
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Функция для выполнения запроса с обработкой редиректов
|
|||
|
|
currentURL := urlStr
|
|||
|
|
|
|||
|
|
for redirectCount := 0; redirectCount < maxRedirects; redirectCount++ {
|
|||
|
|
// Создаем запрос
|
|||
|
|
req, err := http.NewRequest("GET", currentURL, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("ошибка создания запроса: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Устанавливаем заголовки
|
|||
|
|
req.Header.Set("User-Agent", userAgent)
|
|||
|
|
req.Header.Set("Referer", referer)
|
|||
|
|
req.Header.Set("Accept", "*/*")
|
|||
|
|
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
|||
|
|
req.Header.Set("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7")
|
|||
|
|
req.Header.Set("Connection", "keep-alive")
|
|||
|
|
req.Header.Set("Cache-Control", "no-cache")
|
|||
|
|
req.Header.Set("Pragma", "no-cache")
|
|||
|
|
|
|||
|
|
if verbose {
|
|||
|
|
fmt.Printf("\nЗаголовки запроса:\n")
|
|||
|
|
for key, values := range req.Header {
|
|||
|
|
fmt.Printf(" %s: %s\n", key, strings.Join(values, ", "))
|
|||
|
|
}
|
|||
|
|
fmt.Printf("\nВыполнение запроса...\n")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Выполняем запрос
|
|||
|
|
resp, err := client.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("ошибка загрузки: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if verbose {
|
|||
|
|
fmt.Printf("\nСтатус ответа: %s\n", resp.Status)
|
|||
|
|
fmt.Printf("Заголовки ответа:\n")
|
|||
|
|
for key, values := range resp.Header {
|
|||
|
|
fmt.Printf(" %s: %s\n", key, strings.Join(values, ", "))
|
|||
|
|
}
|
|||
|
|
fmt.Println()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверяем статус ответа
|
|||
|
|
if resp.StatusCode == http.StatusOK {
|
|||
|
|
// Успешный ответ - скачиваем файл
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
|
|||
|
|
// Показываем размер файла если известен
|
|||
|
|
if !silent && resp.ContentLength > 0 {
|
|||
|
|
sizeMB := float64(resp.ContentLength) / (1024 * 1024)
|
|||
|
|
fmt.Printf("Размер файла: %.2f MB\n", sizeMB)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Создаем выходной файл
|
|||
|
|
file, err := os.Create(outputFile)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("ошибка создания файла: %v", err)
|
|||
|
|
}
|
|||
|
|
defer file.Close()
|
|||
|
|
|
|||
|
|
// Скачиваем с прогресс-баром или без него
|
|||
|
|
var written int64
|
|||
|
|
if silent {
|
|||
|
|
written, err = io.Copy(file, resp.Body)
|
|||
|
|
} else {
|
|||
|
|
written, err = s.downloadWithProgress(file, resp.Body, resp.ContentLength)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("ошибка сохранения файла: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !silent {
|
|||
|
|
fmt.Printf("\n✅ Загрузка завершена! Сохранено %d байт (%s)\n",
|
|||
|
|
written, formatBytes(written))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.logger.Info("Файл загружен",
|
|||
|
|
"url", urlStr,
|
|||
|
|
"output", outputFile,
|
|||
|
|
"size", written)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
} else if resp.StatusCode == 500 || resp.StatusCode == 200 {
|
|||
|
|
// Читаем тело для поиска meta refresh
|
|||
|
|
body, err := io.ReadAll(resp.Body)
|
|||
|
|
resp.Body.Close()
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("ошибка чтения тела ответа: %v", err)
|
|||
|
|
}
|
|||
|
|
bodyStr := string(body)
|
|||
|
|
|
|||
|
|
// Ищем meta refresh редирект (разные форматы)
|
|||
|
|
patterns := []string{
|
|||
|
|
`<meta[^>]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']`,
|
|||
|
|
`<meta[^>]*content=["']\d+;\s*url=([^"']+)["'][^>]*http-equiv=["']refresh["']`,
|
|||
|
|
`URL\s*=\s*['"]?([^'"\s>]+)`,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
redirectURL := ""
|
|||
|
|
for _, pattern := range patterns {
|
|||
|
|
re := regexp.MustCompile(pattern)
|
|||
|
|
matches := re.FindStringSubmatch(bodyStr)
|
|||
|
|
if len(matches) > 1 {
|
|||
|
|
redirectURL = matches[1]
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if redirectURL != "" {
|
|||
|
|
if verbose {
|
|||
|
|
fmt.Printf("-> HTML meta refresh редирект на: %s\n", redirectURL)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Преобразуем относительный URL в абсолютный
|
|||
|
|
absURL, err := resolveURL(currentURL, redirectURL)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("ошибка преобразования URL редиректа: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if verbose {
|
|||
|
|
fmt.Printf("-> Абсолютный URL: %s\n", absURL)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
currentURL = absURL
|
|||
|
|
continue // Делаем следующий запрос
|
|||
|
|
} else if resp.StatusCode == 500 {
|
|||
|
|
return fmt.Errorf("HTTP ошибка: %s\nТело ответа: %s", resp.Status,
|
|||
|
|
func(s string) string {
|
|||
|
|
if len(s) > 500 {
|
|||
|
|
return s[:500] + "..."
|
|||
|
|
}
|
|||
|
|
return s
|
|||
|
|
}(bodyStr))
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
resp.Body.Close()
|
|||
|
|
return fmt.Errorf("неожиданный HTTP статус: %s", resp.Status)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return fmt.Errorf("превышено максимальное количество редиректов (%d)", maxRedirects)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cmdMycurlHelp показывает справку по команде mycurl
|
|||
|
|
func (s *Shell) cmdMycurlHelp(args []string) error {
|
|||
|
|
fmt.Println()
|
|||
|
|
fmt.Println("mycurl - встроенная утилита для скачивания файлов")
|
|||
|
|
fmt.Println()
|
|||
|
|
fmt.Println("Использование:")
|
|||
|
|
fmt.Println(" mycurl [опции] URL")
|
|||
|
|
fmt.Println()
|
|||
|
|
fmt.Println("Опции:")
|
|||
|
|
fmt.Println(" -o <файл> Сохранить в указанный файл")
|
|||
|
|
fmt.Println(" -O Использовать имя файла из URL")
|
|||
|
|
fmt.Println(" -L Следовать редиректам (включено по умолчанию)")
|
|||
|
|
fmt.Println(" -s Тихий режим (без прогресс-бара)")
|
|||
|
|
fmt.Println(" -v Подробный вывод (заголовки запроса/ответа)")
|
|||
|
|
fmt.Println(" -A <строка> User-Agent строка")
|
|||
|
|
fmt.Println(" -e <строка> Referer строка")
|
|||
|
|
fmt.Println(" -k Игнорировать проверку SSL сертификатов")
|
|||
|
|
fmt.Println(" -h, --help Показать эту справку")
|
|||
|
|
fmt.Println()
|
|||
|
|
fmt.Println("Примеры:")
|
|||
|
|
fmt.Println(" mycurl 'https://example.com/file.zip'")
|
|||
|
|
fmt.Println(" mycurl -o myfile.zip 'https://example.com/file.zip?param=value'")
|
|||
|
|
fmt.Println(" mycurl -O 'https://example.com/file.zip'")
|
|||
|
|
fmt.Println(" mycurl -s 'https://example.com/largefile.iso'")
|
|||
|
|
fmt.Println(" mycurl -v 'https://example.com/debug.zip'")
|
|||
|
|
fmt.Println(" mycurl -A 'CustomAgent/1.0' -e 'https://google.com' 'https://example.com/file.zip'")
|
|||
|
|
fmt.Println(" curl -o file.zip 'https://example.com/file.zip' # алиас")
|
|||
|
|
fmt.Println()
|
|||
|
|
fmt.Println("Примечания:")
|
|||
|
|
fmt.Println(" - Кавычки (одинарные или двойные) ОБЯЗАТЕЛЬНЫ, если URL содержит &, ?, =")
|
|||
|
|
fmt.Println(" - Кавычки автоматически удаляются при обработке")
|
|||
|
|
fmt.Println(" - Поддерживаются cookies и сессии")
|
|||
|
|
fmt.Println(" - Поддерживаются HTML meta refresh редиректы")
|
|||
|
|
fmt.Println(" - Утилита эмулирует поведение curl с заголовками User-Agent и Referer")
|
|||
|
|
fmt.Println(" - Поддерживает автоматическое следование редиректам")
|
|||
|
|
fmt.Println(" - Работает на Linux и OpenIndiana/Hipster")
|
|||
|
|
fmt.Println()
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// downloadWithProgress скачивает с отображением прогресса
|
|||
|
|
func (s *Shell) downloadWithProgress(writer io.Writer, reader io.Reader, total int64) (int64, error) {
|
|||
|
|
progress := &progressWriter{
|
|||
|
|
total: total,
|
|||
|
|
writer: writer,
|
|||
|
|
started: time.Now(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return io.Copy(progress, reader)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// progressWriter реализует запись с прогресс-баром
|
|||
|
|
type progressWriter struct {
|
|||
|
|
total int64
|
|||
|
|
written int64
|
|||
|
|
writer io.Writer
|
|||
|
|
started time.Time
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (pw *progressWriter) Write(p []byte) (int, error) {
|
|||
|
|
n, err := pw.writer.Write(p)
|
|||
|
|
pw.written += int64(n)
|
|||
|
|
|
|||
|
|
// Обновляем прогресс
|
|||
|
|
if pw.total > 0 {
|
|||
|
|
percent := float64(pw.written) / float64(pw.total) * 100
|
|||
|
|
elapsed := time.Since(pw.started)
|
|||
|
|
speed := float64(pw.written) / elapsed.Seconds()
|
|||
|
|
|
|||
|
|
// Скорость в MB/s
|
|||
|
|
speedMBps := speed / (1024 * 1024)
|
|||
|
|
|
|||
|
|
// Оставшееся время
|
|||
|
|
var eta string
|
|||
|
|
if speed > 0 {
|
|||
|
|
remaining := float64(pw.total-pw.written) / speed
|
|||
|
|
if remaining < 60 {
|
|||
|
|
eta = fmt.Sprintf("%.0fs", remaining)
|
|||
|
|
} else if remaining < 3600 {
|
|||
|
|
eta = fmt.Sprintf("%.0fm%.0fs", remaining/60, float64(int(remaining)%60))
|
|||
|
|
} else {
|
|||
|
|
eta = fmt.Sprintf("%.0fh%.0fm", remaining/3600, (int(remaining)%3600)/60)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
eta = "?"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fmt.Printf("\rПрогресс: %.1f%% [%s/%s] @ %.2f MB/s ETA: %s",
|
|||
|
|
percent,
|
|||
|
|
formatBytes(pw.written),
|
|||
|
|
formatBytes(pw.total),
|
|||
|
|
speedMBps,
|
|||
|
|
eta)
|
|||
|
|
} else {
|
|||
|
|
fmt.Printf("\rЗагружено: %s", formatBytes(pw.written))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return n, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// getFilenameFromURL извлекает имя файла из URL
|
|||
|
|
func getFilenameFromURL(urlStr string) string {
|
|||
|
|
// Убираем параметры запроса
|
|||
|
|
if idx := strings.Index(urlStr, "?"); idx != -1 {
|
|||
|
|
urlStr = urlStr[:idx]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Извлекаем последний сегмент пути
|
|||
|
|
parts := strings.Split(urlStr, "/")
|
|||
|
|
if len(parts) > 0 {
|
|||
|
|
filename := parts[len(parts)-1]
|
|||
|
|
if filename != "" && strings.Contains(filename, ".") {
|
|||
|
|
return filename
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// formatBytes форматирует байты в человекочитаемый вид
|
|||
|
|
func formatBytes(bytes int64) string {
|
|||
|
|
const unit = 1024
|
|||
|
|
if bytes < unit {
|
|||
|
|
return fmt.Sprintf("%d B", bytes)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
div, exp := int64(unit), 0
|
|||
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|||
|
|
div *= unit
|
|||
|
|
exp++
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch exp {
|
|||
|
|
case 0:
|
|||
|
|
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(div))
|
|||
|
|
case 1:
|
|||
|
|
return fmt.Sprintf("%.1f MB", float64(bytes)/float64(div))
|
|||
|
|
case 2:
|
|||
|
|
return fmt.Sprintf("%.1f GB", float64(bytes)/float64(div))
|
|||
|
|
default:
|
|||
|
|
return fmt.Sprintf("%.1f TB", float64(bytes)/float64(div))
|
|||
|
|
}
|
|||
|
|
}
|