[mike@maginot]
This commit is contained in:
@@ -50,6 +50,50 @@ Braucht Go ≥ 1.26 und `github.com/fatih/color`. Das Perl-Original (`dx`, 1.0.2
|
|||||||
2016) liegt unverändert daneben und wird vom Bau nicht angefasst; es hat keine
|
2016) liegt unverändert daneben und wird vom Bau nicht angefasst; es hat keine
|
||||||
Farben und keine Balken, tut aber sonst dasselbe.
|
Farben und keine Balken, tut aber sonst dasselbe.
|
||||||
|
|
||||||
|
## Selbst aktualisieren
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dx --check-update # nur nachsehen
|
||||||
|
dx --update # holen und ersetzen
|
||||||
|
```
|
||||||
|
|
||||||
|
`dx` holt sich das neueste Release von
|
||||||
|
[git.micw.org/mike/dx](https://git.micw.org/mike/dx) — die URL steht fest im
|
||||||
|
Programm, es gibt nichts zu konfigurieren. Gebraucht wird ein Release je
|
||||||
|
Version, dessen Tag die nackte Nummer ist (`2.1.6`), mit den Dateien aus `./bin`
|
||||||
|
als Assets; gesucht wird die eine, die zu `GOOS`/`GOARCH` dieser Maschine passt.
|
||||||
|
|
||||||
|
Ersetzt wird die laufende Datei selbst. Ist der aufgerufene `dx` ein Symlink —
|
||||||
|
etwa `~/bin/dx` auf `~/src/dx/bin/dx-darwin-arm64` —, wird das Ziel dahinter
|
||||||
|
erneuert, nicht der Link. Vor dem Tausch wird das frisch Geladene einmal mit
|
||||||
|
`--version` aufgerufen; meldet es nicht die erwartete Nummer, bleibt alles, wie
|
||||||
|
es war. Der Tausch selbst ist ein `rename` im selben Verzeichnis, also atomar:
|
||||||
|
entweder die alte Datei oder die neue, nie eine halbe.
|
||||||
|
|
||||||
|
Liegt das Binary irgendwo, wo der eigene Benutzer nicht schreiben darf
|
||||||
|
(`/usr/local/bin`), sagt `dx --update` das und tut nichts — dann `sudo`.
|
||||||
|
|
||||||
|
## Wiederverwenden
|
||||||
|
|
||||||
|
`selfupdate.go` hängt an nichts im Rest des Programms und kommt mit der
|
||||||
|
Standardbibliothek aus. Für ein anderes Programm die Datei kopieren, den Block
|
||||||
|
oben anpassen —
|
||||||
|
|
||||||
|
```go
|
||||||
|
var selfUpdate = selfUpdater{
|
||||||
|
repo: "https://git.micw.org/mike/foo",
|
||||||
|
asset: "foo",
|
||||||
|
current: version,
|
||||||
|
verify: []string{"--version"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
— und `--update` und `--check-update` in die Optionen hängen. Alles Weitere in
|
||||||
|
der Datei heißt `selfUpdate…` oder `update…` und kollidiert deshalb nicht.
|
||||||
|
`verify` leer lassen, wenn das Programm keinen billigen Versionsaufruf hat; dann
|
||||||
|
entfällt der Probelauf. Erwartet wird eine Gitea-Instanz (getestet mit 1.27);
|
||||||
|
GitHub kennt dieselbe Route unter anderen Feldnamen und wird nicht bedient.
|
||||||
|
|
||||||
## Optionen
|
## Optionen
|
||||||
|
|
||||||
| | | |
|
| | | |
|
||||||
@@ -62,6 +106,8 @@ Farben und keine Balken, tut aber sonst dasselbe.
|
|||||||
| | `--bar`, `--no-bar` | Balken und Prozent erzwingen bzw. abschalten |
|
| | `--bar`, `--no-bar` | Balken und Prozent erzwingen bzw. abschalten |
|
||||||
| | `--color=auto\|always\|never` | Farbe erzwingen bzw. abschalten |
|
| | `--color=auto\|always\|never` | Farbe erzwingen bzw. abschalten |
|
||||||
| | `--version` | Version |
|
| | `--version` | Version |
|
||||||
|
| | `--check-update` | nachsehen, ob ein neueres Release da ist |
|
||||||
|
| | `--update` | neuestes Release holen und installieren |
|
||||||
|
|
||||||
Kurzoptionen lassen sich bündeln (`-rt 3`, `-dn`), der Wert von `-t` darf kleben
|
Kurzoptionen lassen sich bündeln (`-rt 3`, `-dn`), der Wert von `-t` darf kleben
|
||||||
(`-t5`) oder folgen (`-t 5`). Ein `--` trennt Optionen von einem Pfad, der mit
|
(`-t5`) oder folgen (`-t 5`). Ein `--` trennt Optionen von einem Pfad, der mit
|
||||||
@@ -163,6 +209,7 @@ Neu sind `-r`, `-t`, die Balkenspalte, die Farben und die langen Optionen.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `main.go` | Zählen, Sortieren, Formatieren, Optionen |
|
| `main.go` | Zählen, Sortieren, Formatieren, Optionen |
|
||||||
| `filetypes.go` | Endung und Dateiname → Farbe |
|
| `filetypes.go` | Endung und Dateiname → Farbe |
|
||||||
|
| `selfupdate.go` | `--update`, eigenständig und zum Kopieren gedacht |
|
||||||
| `build.sh` | Bau für alle Plattformen, zählt `version.txt` hoch |
|
| `build.sh` | Bau für alle Plattformen, zählt `version.txt` hoch |
|
||||||
| `version.txt` | die zuletzt gebaute Version |
|
| `version.txt` | die zuletzt gebaute Version |
|
||||||
| `dx` | das Perl-Original von 2016 |
|
| `dx` | das Perl-Original von 2016 |
|
||||||
|
|||||||
@@ -383,6 +383,8 @@ usage: dx [-n] [-d] [-r] [-t N] [path]
|
|||||||
--bar / --no-bar share bar and percentage (default: when colorized)
|
--bar / --no-bar share bar and percentage (default: when colorized)
|
||||||
--color=auto|always|never colorize output (default auto)
|
--color=auto|always|never colorize output (default auto)
|
||||||
--version
|
--version
|
||||||
|
--check-update look for a newer release
|
||||||
|
--update download and install the newest release
|
||||||
`, version)
|
`, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +414,18 @@ func main() {
|
|||||||
case a == "--version":
|
case a == "--version":
|
||||||
fmt.Printf("dx %s\n", version)
|
fmt.Printf("dx %s\n", version)
|
||||||
return
|
return
|
||||||
|
case a == "--update":
|
||||||
|
if err := selfUpdate.install(os.Stdout); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "dx: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case a == "--check-update":
|
||||||
|
if err := selfUpdate.check(os.Stdout); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "dx: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
case a == "--number":
|
case a == "--number":
|
||||||
o.byCount = true
|
o.byCount = true
|
||||||
case a == "--dirs":
|
case a == "--dirs":
|
||||||
|
|||||||
+349
@@ -0,0 +1,349 @@
|
|||||||
|
// selfupdate.go — Selbstaktualisierung aus den Releases einer Gitea-Instanz.
|
||||||
|
//
|
||||||
|
// Die Datei ist als Kopiervorlage gedacht: in ein anderes Programm übernehmen,
|
||||||
|
// den Konfigurationsblock unten anpassen, `--update` und `--check-update` in
|
||||||
|
// die Optionen hängen — fertig. Sie braucht nur die Standardbibliothek und
|
||||||
|
// bringt außer dem Block keine Namen mit, die nicht mit "selfUpdate" oder
|
||||||
|
// "update" beginnen.
|
||||||
|
//
|
||||||
|
// Vorausgesetzt wird das Ablageschema von build.sh: je Version ein Release,
|
||||||
|
// dessen Tag die nackte Nummer ist (2.1.6, ein führendes "v" ist erlaubt), und
|
||||||
|
// darin je ein Asset "<name>-<goos>-<goarch>" — also genau die Dateien aus
|
||||||
|
// ./bin. Gitea liefert unter /api/v1/repos/<owner>/<repo>/releases/latest das
|
||||||
|
// neueste Release ohne Entwurf und ohne Vorabversion; GitHub spricht dieselbe
|
||||||
|
// Route mit anderen Feldnamen und ist deshalb nicht mitgemeint.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ Konfiguration
|
||||||
|
|
||||||
|
var selfUpdate = selfUpdater{
|
||||||
|
repo: "https://git.micw.org/mike/dx",
|
||||||
|
asset: "dx",
|
||||||
|
current: version, // aus main.go, per -ldflags gesetzt
|
||||||
|
verify: []string{"--version"},
|
||||||
|
}
|
||||||
|
|
||||||
|
type selfUpdater struct {
|
||||||
|
repo string // Repo-URL wie im Browser: https://host/owner/repo
|
||||||
|
asset string // Basisname der Assets, "-<goos>-<goarch>" kommt dazu
|
||||||
|
current string // laufende Version
|
||||||
|
verify []string // Probelauf des Downloads; leer lässt ihn ausfallen
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ Ablauf
|
||||||
|
|
||||||
|
// check sieht nur nach und fasst nichts an.
|
||||||
|
func (u selfUpdater) check(w io.Writer) error {
|
||||||
|
rel, err := u.latest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if updateCompare(rel.TagName, u.current) <= 0 {
|
||||||
|
fmt.Fprintf(w, "%s %s is up to date\n", u.asset, u.current)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%s %s is available, running %s\n %s\n run '%s --update' to install it\n",
|
||||||
|
u.asset, rel.TagName, u.current, rel.HTMLURL, u.asset)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// install holt das neueste Release und ersetzt die laufende Datei damit.
|
||||||
|
func (u selfUpdater) install(w io.Writer) error {
|
||||||
|
rel, err := u.latest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if updateCompare(rel.TagName, u.current) <= 0 {
|
||||||
|
fmt.Fprintf(w, "%s %s is up to date\n", u.asset, u.current)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
want := fmt.Sprintf("%s-%s-%s", u.asset, runtime.GOOS, runtime.GOARCH)
|
||||||
|
var src *updateAsset
|
||||||
|
for i := range rel.Assets {
|
||||||
|
if rel.Assets[i].Name == want {
|
||||||
|
src = &rel.Assets[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src == nil {
|
||||||
|
names := make([]string, len(rel.Assets))
|
||||||
|
for i, a := range rel.Assets {
|
||||||
|
names[i] = a.Name
|
||||||
|
}
|
||||||
|
return fmt.Errorf("release %s has no %q (only %s)", rel.TagName, want, strings.Join(names, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot locate the running binary: %w", err)
|
||||||
|
}
|
||||||
|
// Ein installiertes dx ist oft ein Symlink nach ./bin. Ersetzt werden soll
|
||||||
|
// die Datei dahinter, nicht der Link.
|
||||||
|
if real, err := filepath.EvalSymlinks(exe); err == nil {
|
||||||
|
exe = real
|
||||||
|
}
|
||||||
|
mode := os.FileMode(0o755)
|
||||||
|
if st, err := os.Stat(exe); err == nil {
|
||||||
|
mode = st.Mode().Perm()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "downloading %s %s (%s)\n", want, rel.TagName, updateSize(src.Size))
|
||||||
|
tmp, err := u.download(src, exe, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmp) // greift nur, wenn das Umbenennen unten ausfällt
|
||||||
|
|
||||||
|
if err := u.probe(tmp, rel.TagName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := updateReplace(tmp, exe); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s %s → %s, at %s\n", u.asset, u.current, rel.TagName, exe)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u selfUpdater) download(a *updateAsset, exe string, mode os.FileMode) (string, error) {
|
||||||
|
// Die neue Datei entsteht neben der alten: dasselbe Dateisystem, also ist
|
||||||
|
// das Umbenennen am Ende ein atomarer Schritt und kein halber Kopiervorgang.
|
||||||
|
// Sie entsteht auch vor dem ersten Byte — ein fehlendes Schreibrecht soll
|
||||||
|
// auffallen, bevor ein paar Megabyte durch die Leitung sind.
|
||||||
|
dir := filepath.Dir(exe)
|
||||||
|
f, err := os.CreateTemp(dir, "."+filepath.Base(exe)+".new")
|
||||||
|
if err != nil {
|
||||||
|
var pe *os.PathError // der Pfad steht schon in der Meldung
|
||||||
|
if errors.As(err, &pe) {
|
||||||
|
err = pe.Err
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("cannot write to %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
tmp := f.Name()
|
||||||
|
|
||||||
|
resp, err := updateGet(a.URL)
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(tmp)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
n, err := io.Copy(f, resp.Body)
|
||||||
|
if cerr := f.Close(); err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
if err == nil && a.Size > 0 && n != a.Size {
|
||||||
|
err = fmt.Errorf("got %d of %d bytes from %s", n, a.Size, a.URL)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = os.Chmod(tmp, mode)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tmp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// probe ruft den frisch geladenen Läufer einmal auf. Das fängt eine
|
||||||
|
// abgeschnittene, für die falsche Plattform gebaute oder gar nicht erst
|
||||||
|
// ausführbare Datei ab, bevor sie die laufende ersetzt.
|
||||||
|
func (u selfUpdater) probe(path, tag string) error {
|
||||||
|
if len(u.verify) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
out, err := exec.CommandContext(ctx, path, u.verify...).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("the downloaded binary does not run: %w", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), strings.TrimPrefix(tag, "v")) {
|
||||||
|
return fmt.Errorf("the downloaded binary reports %q, expected %s",
|
||||||
|
strings.TrimSpace(string(out)), tag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateReplace tauscht die laufende Datei gegen die neue.
|
||||||
|
func updateReplace(tmp, exe string) error {
|
||||||
|
if err := os.Rename(tmp, exe); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Unix überschreibt die Datei eines laufenden Programms klaglos, Windows
|
||||||
|
// nicht: dort muss die alte erst aus dem Weg. Löschen lässt sie sich
|
||||||
|
// frühestens, wenn dieser Prozess endet — das Aufräumen darf also scheitern.
|
||||||
|
old := exe + ".old"
|
||||||
|
os.Remove(old)
|
||||||
|
if err := os.Rename(exe, old); err != nil {
|
||||||
|
return fmt.Errorf("cannot replace %s: %w", exe, err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, exe); err != nil {
|
||||||
|
os.Rename(old, exe) // zurück auf den alten Stand
|
||||||
|
return fmt.Errorf("cannot replace %s: %w", exe, err)
|
||||||
|
}
|
||||||
|
os.Remove(old)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------- Gitea
|
||||||
|
|
||||||
|
type updateRelease struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
Assets []updateAsset `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateAsset struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
URL string `json:"browser_download_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u selfUpdater) latest() (updateRelease, error) {
|
||||||
|
base, err := u.apiBase()
|
||||||
|
if err != nil {
|
||||||
|
return updateRelease{}, err
|
||||||
|
}
|
||||||
|
resp, err := updateGet(base + "/releases/latest")
|
||||||
|
if err != nil {
|
||||||
|
return updateRelease{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var rel updateRelease
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
||||||
|
return updateRelease{}, fmt.Errorf("unexpected answer from %s: %w", base, err)
|
||||||
|
}
|
||||||
|
if rel.TagName == "" {
|
||||||
|
return updateRelease{}, fmt.Errorf("%s has no releases", u.repo)
|
||||||
|
}
|
||||||
|
return rel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiBase macht aus https://host/owner/repo die API-Wurzel des Repos.
|
||||||
|
func (u selfUpdater) apiBase() (string, error) {
|
||||||
|
bad := fmt.Errorf("repo %q: expected https://host/owner/repo", u.repo)
|
||||||
|
|
||||||
|
ref, err := url.Parse(strings.TrimSuffix(strings.TrimSuffix(u.repo, "/"), ".git"))
|
||||||
|
if err != nil || ref.Host == "" {
|
||||||
|
return "", bad
|
||||||
|
}
|
||||||
|
parts := strings.Split(strings.Trim(ref.Path, "/"), "/")
|
||||||
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||||
|
return "", bad
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s://%s/api/v1/repos/%s/%s", ref.Scheme, ref.Host, parts[0], parts[1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ein Zeitlimit für alles zusammen: die Suche kostet ein paar hundert
|
||||||
|
// Millisekunden, der Download ein paar Megabyte — beides darf hängen bleiben,
|
||||||
|
// aber nicht ewig.
|
||||||
|
var updateClient = &http.Client{Timeout: 5 * time.Minute}
|
||||||
|
|
||||||
|
func updateGet(target string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, target, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "selfupdate.go (+"+runtime.GOOS+"/"+runtime.GOARCH+")")
|
||||||
|
|
||||||
|
resp, err := updateClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("GET %s: %s", target, resp.Status)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ Nummern
|
||||||
|
|
||||||
|
// updateCompare vergleicht zwei Versionen komponentenweise numerisch, damit
|
||||||
|
// 2.1.10 hinter 2.1.9 landet und nicht davor. Ein führendes "v" zählt nicht,
|
||||||
|
// fehlende Stellen gelten als 0 (2.1 == 2.1.0), und ein Suffix am Zahlenrest
|
||||||
|
// macht die Version älter, nicht neuer (2.1.6-rc1 < 2.1.6). Ergebnis wie bei
|
||||||
|
// strings.Compare: -1, 0, 1.
|
||||||
|
func updateCompare(a, b string) int {
|
||||||
|
as := strings.Split(strings.TrimPrefix(a, "v"), ".")
|
||||||
|
bs := strings.Split(strings.TrimPrefix(b, "v"), ".")
|
||||||
|
|
||||||
|
for i := 0; i < len(as) || i < len(bs); i++ {
|
||||||
|
x, y := "0", "0"
|
||||||
|
if i < len(as) {
|
||||||
|
x = as[i]
|
||||||
|
}
|
||||||
|
if i < len(bs) {
|
||||||
|
y = bs[i]
|
||||||
|
}
|
||||||
|
if c := updateComparePart(x, y); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateComparePart(a, b string) int {
|
||||||
|
na, ra := updateSplitNum(a)
|
||||||
|
nb, rb := updateSplitNum(b)
|
||||||
|
switch {
|
||||||
|
case na != nb:
|
||||||
|
if na < nb {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
case ra == rb:
|
||||||
|
return 0
|
||||||
|
case ra == "": // 2.1.6 ist fertig, 2.1.6-rc1 noch nicht
|
||||||
|
return 1
|
||||||
|
case rb == "":
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return strings.Compare(ra, rb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSplitNum trennt "10-rc1" in 10 und "-rc1".
|
||||||
|
func updateSplitNum(s string) (int, string) {
|
||||||
|
i := 0
|
||||||
|
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
n, _ := strconv.Atoi(s[:i])
|
||||||
|
return n, s[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSize ist bewusst eine eigene kleine Formatierung und nicht formSize aus
|
||||||
|
// main.go — die Datei soll für sich alleine stehen.
|
||||||
|
func updateSize(b int64) string {
|
||||||
|
const k = 1024
|
||||||
|
switch {
|
||||||
|
case b > k*k:
|
||||||
|
return fmt.Sprintf("%.1f MB", float64(b)/k/k)
|
||||||
|
case b > k:
|
||||||
|
return fmt.Sprintf("%.1f KB", float64(b)/k)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.1.4
|
2.1.8
|
||||||
|
|||||||
Reference in New Issue
Block a user