diff --git a/internal/shell/commands.go b/internal/shell/commands.go new file mode 100644 index 0000000..bea286d --- /dev/null +++ b/internal/shell/commands.go @@ -0,0 +1,804 @@ +// 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] ", "Создать новую директорию"}, + {"rm [-rf] ", "Удалить файл или директорию"}, + {"touch ", "Создать файл или обновить время доступа"}, + {"cat ", "Вывести содержимое файла"}, + {"echo [text...]", "Вывести текст на экран"}, + {"exec [args...]", "Выполнить внешнюю команду"}, + {"mycurl [опции] ", "Скачать файл (аналог 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 [опции] \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{ + `]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']`, + `]*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)) + } +}