2 Commits
Author SHA1 Message Date
Matteo Benedetto 5f82187c67 progress: barra di avanzamento per download ed estrazione
Aggiunge src/progress.{c,h}: barra a una riga attiva solo quando stdout e' un
terminale (con output rediretto/pipeline non stampa nulla, quindi l'output resta
identico al client Python: parita' 30/30 invariata).

- due fasi: "Scaricamento" (byte da Content-Length) ed "Estrazione" (byte
  compressi consumati + membri processati: nuovo contatore in gz_reader e
  callback tar_progress_fn/tar_extract_cb)
- controllo con CELLAR_PROGRESS=auto|bar|plain|off (default auto)
- nessun codice ANSI, solo '\r' e riempimento con spazi; glifi ASCII se la
  locale non e' UTF-8, larghezza da TIOCGWINSZ e misurata in colonne (i glifi
  UTF-8 sono multi-byte), ridisegno throttled, velocita' a media mobile, ETA
- percorsi d'errore: progress_abort() chiude la riga senza riepilogo; il file
  parziale resta come nel client Python
- tests/progress_test.sh: pipe silenziosa, PTY via script (barra, ETA, riepilogo,
  nessun ANSI, righe entro la larghezza), modalita' plain e off, integrita' del
  file scaricato e della bottiglia installata (13/13 verdi)
- tests/parity_test.sh resta 30/30; mock_server.py con --throttle per i test
2026-09-20 17:46:29 +02:00
Matteo Benedetto 6f5b224b2f docs: sezione download dei binari statici della release v1.0.0 + SHA256SUMS 2026-09-20 17:31:12 +02:00
43 changed files with 781 additions and 14 deletions
+2 -1
View File
@@ -50,7 +50,8 @@ verify: all
tests/verify_binary.sh $(DIST)/$(NAME)
test: asan
tests/parity_test.sh
tests/parity_test.sh # parita' col client Python (30 casi + artefatti)
tests/progress_test.sh # barra di avanzamento (pipe/PTY/plain/off)
clean:
rm -rf $(DIST) build tests/tmp
+56 -3
View File
@@ -17,6 +17,20 @@ Autore: Matteo Benedetto — progetto derivato da `enne2/cellar` (commit `f5216b
| Build | `x86_64` static, `i686` static, `aarch64` static (cross) |
| Installazione locale | `~/.local/bin/cellar-cli` → symlink a `dist/cellar-cli` |
## Download dei binari pronti
Nella release **v1.0.0** sono allegati tre binari statici (nessuna dipendenza a runtime):
| file | architettura | ABI minima |
|---|---|---|
| `cellar-cli-1.0.0-linux-x86_64-static` | x86-64 (amd64) | Linux, ABI 3.2.0 |
| `cellar-cli-1.0.0-linux-i686-static` | i686 (x86 32 bit) | Linux, ABI 3.2.0 |
| `cellar-cli-1.0.0-linux-aarch64-static` | aarch64 (arm64) | Linux, ABI 3.7.0 |
Insieme a `SHA256SUMS.txt` per la verifica: `sha256sum -c SHA256SUMS.txt`.
Pubblicato su Gitea: <https://git.enne2.net/enne2/cellar-cli-c/releases/tag/v1.0.0>
## Build
```bash
@@ -67,6 +81,37 @@ cellar-cli --server http://10.8.0.3:8080 list # override del server
Exit code: `0` ok, `1` errore HTTP/rete/file, `2` argomenti non validi (come argparse).
## Barra di avanzamento
Durante `install`, `wizard-install` e `download` viene mostrata una barra su una riga,
con due fasi (`Scaricamento` ed `Estrazione`):
```
Scaricamento [████████████░░░░░░░░░░░░] 58% 6.7 MB/11.5 MB 3.0 MB/s ETA 00:01
Estrazione [████████████████████████] 100% 11.5 MB/11.5 MB 178 MB/s membri: 20
```
- **attiva solo se stdout è un terminale**: con output rediretto, in pipeline, in cron o
nei log **non stampa nulla** — per questo l'output resta identico al client Python;
- controllo con la variabile d'ambiente **`CELLAR_PROGRESS`**:
- `auto` (default) → barra su terminale, silenzio altrove;
- `bar` → barra anche con output rediretto;
- `plain` → una riga ogni 10% (per log e cron, senza disegno);
- `off` → disattivata;
- **nessun codice ANSI**: solo `\r` e riempimento con spazi, quindi funziona anche su
console vecchie e seriali; glifi ASCII (`#`/`-`) se la locale non è UTF-8, blocchi
Unicode (`█`/`░`) se lo è;
- larghezza adattata al terminale (`TIOCGWINSZ`, fallback 80 colonne, max 200) e misurata
in **colonne**, non in byte (i glifi UTF-8 sono multi-byte);
- ridisegno limitato a ~8 volte al secondo, velocità con media mobile, ETA mostrata solo
quando la stima è affidabile;
- `CELLAR_PROGRESS_DEBUG=1` stampa su stderr modalità/larghezza calcolate (diagnostica).
```bash
CELLAR_PROGRESS=plain cellar-cli install 'Gioco' # output adatto a un log
CELLAR_PROGRESS=off cellar-cli install 'Gioco' # nessuna barra
```
## Retrocompatibilità (le scelte che contano)
- **Binario statico** (`-static`): nessuna dipendenza da glibc a runtime, quindi gira su
@@ -114,6 +159,7 @@ sei casi di errore di argomenti, più verifiche sugli artefatti:
| Confronto nomi case-insensitive | ASCII (`tolower`) invece di `str.lower()` Unicode |
| EOF su una richiesta interattiva | Python solleva `EOFError` (exit 1); qui si stampa `EOFError: EOF when reading a line` e si esce **1**. Nei prompt con default (nome archivio, descrizione…) su EOF si applica il default, così i wizard restano usabili da script |
| Upload multipart | In **streaming** (il client Python legge l'intero archivio in RAM) |
| Barra di avanzamento | Aggiunta (il CLI Python non stampa nulla durante download/estrazione). Attiva solo su terminale, quindi l'output rediretto resta identico all'originale |
| TLS | Non supportato (niente OpenSSL): serve `http://`, oppure un reverse proxy TLS. Il server Cellar è HTTP |
| Timeout di rete | Connect 15 s, I/O 300 s (urllib non ha timeout) |
| Redirect | Seguiti come `urllib.request`: GET/HEAD su 301/302/303/307/308, POST→GET su 301/302/303 |
@@ -121,11 +167,17 @@ sei casi di errore di argomenti, più verifiche sugli artefatti:
## Test e verifica
```bash
make test # parità (usa la build ASan)
C_BIN=dist/cellar-cli tests/parity_test.sh # parità con il binario statico
make verify # file/ldd/ABI/simboli GLIBC/syscall/smoke test
make test # parità + barra (build ASan)
C_BIN=dist/cellar-cli tests/parity_test.sh # parità con il binario statico
C_BIN=dist/cellar-cli tests/progress_test.sh # barra: pipe silenziosa, PTY, plain, off
make verify # file/ldd/ABI/simboli GLIBC/syscall/smoke test
```
`tests/progress_test.sh` usa un server mock con throttling (`--throttle` byte/s) e
`script -qec` per allocare un PTY: verifica che su pipe non ci sia alcun output di
progresso, che su terminale compaiano le due fasi con ETA e riepilogo, che nessuna riga
superi la larghezza del terminale e che non ci siano codici ANSI.
## Struttura
```
@@ -140,6 +192,7 @@ src/config.{c,h} ~/.cellar.conf (INI in stile configparser)
src/bottle.{c,h} record archivi, scansione bottiglie, bottle.yml, backup
src/ui.{c,h} tabelle e JSON con larghezze in code point (come le f-string)
src/ops.{c,h} list/upload/download/install
src/progress.{c,h} barra di avanzamento (download + estrazione, env CELLAR_PROGRESS)
src/wizard.{c,h} flussi interattivi
src/main.c CLI e messaggi di errore in stile argparse
third_party/ miniz (unlicense/MIT) + licenza
+3
View File
@@ -0,0 +1,3 @@
0c454b2c65ac872efcd19888f996f3de17e6d69541cfc8b2657c0a0bfaf9b23c cellar-cli-1.0.0-linux-x86_64-static
16c7db8c6667ff46c4a276bf7985b2ebc1cb5078f079651414472d47ef5f9ad7 cellar-cli-1.0.0-linux-i686-static
11ad5ae2b5916c21d0ed2f5fa7a640e787596da2df4f5d3685b1bb48505d8f7b cellar-cli-1.0.0-linux-aarch64-static
+17
View File
@@ -30,6 +30,7 @@ struct gz_reader {
mz_stream zs;
FILE *fp;
char in[GZ_BUFSZ];
unsigned long long total_in; /* byte compressi consumati */
int stream_end;
unsigned long crc;
unsigned long long total_out;
@@ -247,6 +248,11 @@ gz_reader *gzr_open(const char *path, cerror *e)
free(r);
return NULL;
}
/* i byte dell'header fanno parte dell'avanzamento */
{
long pos = ftell(r->fp);
r->total_in = (pos > 0) ? (unsigned long long)pos : 0;
}
int rc = mz_inflateInit2(&r->zs, -15); /* deflate raw */
if (rc != MZ_OK) {
err_set(e, ERR_MISC, 0, "inflate init failed (%d)", rc);
@@ -286,6 +292,7 @@ long gzr_read(gz_reader *r, void *buf, size_t cap, cerror *e)
}
break; /* EOF inatteso: lo segnala il chiamante */
}
r->total_in += (unsigned long long)n;
r->zs.next_in = (unsigned char *)r->in;
r->zs.avail_in = (unsigned int)n;
}
@@ -310,6 +317,16 @@ long gzr_read(gz_reader *r, void *buf, size_t cap, cerror *e)
return (long)produced;
}
void gzr_progress(gz_reader *r, long long *in_bytes, long long *out_bytes)
{
if (!r)
return;
if (in_bytes)
*in_bytes = (long long)r->total_in;
if (out_bytes)
*out_bytes = (long long)r->total_out;
}
void gzr_close(gz_reader *r)
{
if (!r)
+2
View File
@@ -19,6 +19,8 @@ int gzw_close(gz_writer *w, cerror *e);
gz_reader *gzr_open(const char *path, cerror *e);
/* Ritorna il numero di byte letti, 0 a EOF, -1 su errore. */
long gzr_read(gz_reader *r, void *buf, size_t cap, cerror *e);
/* Avanzamento: byte compressi consumati e byte decompressi prodotti. */
void gzr_progress(gz_reader *r, long long *in_bytes, long long *out_bytes);
void gzr_close(gz_reader *r);
#endif /* CELLAR_GZIP_H */
+39 -5
View File
@@ -4,6 +4,7 @@
#include "fsutil.h"
#include "http.h"
#include "json.h"
#include "progress.h"
#include "tar.h"
#include "ui.h"
@@ -141,6 +142,7 @@ int ops_upload(const char *server, const char *filepath, const mp_field *fields,
typedef struct {
FILE *fp;
long long written;
progress *pr; /* opzionale: barra di avanzamento */
} file_sink;
static int sink_write(void *ctx, const char *buf, size_t len, cerror *e)
@@ -151,6 +153,8 @@ static int sink_write(void *ctx, const char *buf, size_t len, cerror *e)
return -1;
}
s->written += (long long)len;
if (s->pr)
progress_update(s->pr, s->written);
return 0;
}
@@ -187,9 +191,12 @@ static int download_to(const char *server, long long archive_id, const char *des
csresponse_free(&meta);
return -1;
}
progress pr;
progress_init(&pr, "Scaricamento", meta.content_length);
file_sink sink;
sink.fp = fp;
sink.written = 0;
sink.pr = &pr;
int rc = http_read_body(c, sink_write, &sink, NULL, e);
if (fclose(fp) != 0 && rc == 0) {
err_set_errno(e, ERR_FILE, "cannot write %s", dest);
@@ -197,8 +204,11 @@ static int download_to(const char *server, long long archive_id, const char *des
}
http_close(c);
csresponse_free(&meta);
if (rc != 0)
return -1;
if (rc != 0) {
progress_abort(&pr);
return -1; /* come il client Python: il file parziale resta */
}
progress_finish(&pr);
if (announce)
printf("Downloaded to %s\n", dest);
return 0;
@@ -252,9 +262,12 @@ int ops_download(const char *server, long long archive_id, const char *output, c
csresponse_free(&meta);
return -1;
}
progress pr;
progress_init(&pr, "Scaricamento", meta.content_length);
file_sink sink;
sink.fp = fp;
sink.written = 0;
sink.pr = &pr;
int rc = http_read_body(c, sink_write, &sink, NULL, e);
if (fclose(fp) != 0 && rc == 0) {
err_set_errno(e, ERR_FILE, "cannot write %s", dest);
@@ -262,14 +275,25 @@ int ops_download(const char *server, long long archive_id, const char *output, c
}
http_close(c);
csresponse_free(&meta);
if (rc != 0)
if (rc != 0) {
progress_abort(&pr);
return -1;
}
progress_finish(&pr);
printf("Downloaded to %s\n", dest);
return 0;
}
return download_to(server, archive_id, output, 1, e);
}
static int extract_progress_cb(void *ctx, long long consumed, long long members)
{
progress *p = ctx;
progress_set_members(p, members);
progress_update(p, consumed);
return 0;
}
int ops_install_record(const char *server, const archive_rec *rec, const char *bottles_dir, int replace,
const char *ref, cerror *e)
{
@@ -319,8 +343,18 @@ int ops_install_record(const char *server, const archive_rec *rec, const char *b
if (mkdir_p(extract_dir, e) != 0)
goto cleanup;
if (tar_extract(download_path, extract_dir, e) != 0)
goto cleanup;
{
struct stat arc_st;
long long arc_size = (stat(download_path, &arc_st) == 0) ? (long long)arc_st.st_size : -1;
progress px;
progress_init(&px, "Estrazione", arc_size);
int xrc = tar_extract_cb(download_path, extract_dir, extract_progress_cb, &px, e);
if (xrc != 0) {
progress_abort(&px);
goto cleanup;
}
progress_finish(&px);
}
strlist subdirs;
strlist_init(&subdirs);
+336
View File
@@ -0,0 +1,336 @@
/* progress.c — implementazione della barra (vd. progress.h). */
#include "progress.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
#define PROG_MIN_WIDTH 40
#define PROG_MAX_WIDTH 200
#define PROG_DRAW_INTERVAL 0.12 /* secondi tra due ridisegni */
#define PROG_SAMPLE_INTERVAL 0.25
#define PROG_ASCII_FULL '#'
#define PROG_ASCII_EMPTY '-'
#define PROG_UNI_FULL "\xe2\x96\x88" /* U+2588 */
#define PROG_UNI_EMPTY "\xe2\x96\x91" /* U+2591 */
static double now_monotonic(void)
{
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
return 0.0;
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
static int env_is_utf8(void)
{
const char *vars[] = {getenv("LC_ALL"), getenv("LC_CTYPE"), getenv("LANG")};
for (size_t i = 0; i < sizeof(vars) / sizeof(vars[0]); i++) {
const char *v = vars[i];
if (!v || !*v)
continue;
char buf[64];
size_t n = strlen(v);
if (n >= sizeof(buf))
n = sizeof(buf) - 1;
for (size_t k = 0; k < n; k++)
buf[k] = (char)tolower((unsigned char)v[k]);
buf[n] = '\0';
if (strstr(buf, "utf-8") || strstr(buf, "utf8"))
return 1;
/* la prima variabile impostata vince (come fa setlocale) */
return 0;
}
return 0;
}
prog_mode progress_mode_from_env(void)
{
const char *v = getenv("CELLAR_PROGRESS");
if (!v || !*v)
return PROG_AUTO;
if (!strcmp(v, "bar") || !strcmp(v, "1") || !strcmp(v, "on") || !strcmp(v, "yes"))
return PROG_BAR;
if (!strcmp(v, "plain") || !strcmp(v, "line"))
return PROG_PLAIN;
if (!strcmp(v, "off") || !strcmp(v, "0") || !strcmp(v, "none") || !strcmp(v, "no"))
return PROG_OFF;
return PROG_AUTO;
}
static int terminal_width(void)
{
struct winsize ws;
if (isatty(STDOUT_FILENO) && ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
int w = ws.ws_col;
if (w < PROG_MIN_WIDTH)
w = PROG_MIN_WIDTH;
if (w > PROG_MAX_WIDTH)
w = PROG_MAX_WIDTH;
return w;
}
return 80;
}
/* Dimensione in stile client Python: MiB etichettati MB. */
static void fmt_size(long long bytes, char *out, size_t outsz)
{
double b = (double)bytes;
if (bytes < 1024)
snprintf(out, outsz, "%lld B", bytes);
else if (bytes < 1024 * 1024)
snprintf(out, outsz, "%.0f kB", b / 1024.0);
else if (bytes < 1024LL * 1024 * 1024)
snprintf(out, outsz, "%.1f MB", b / (1024.0 * 1024.0));
else
snprintf(out, outsz, "%.2f GB", b / (1024.0 * 1024.0 * 1024.0));
}
static void fmt_speed(double bytes_per_sec, char *out, size_t outsz)
{
double b = bytes_per_sec;
if (b < 1024)
snprintf(out, outsz, "%.0f B/s", b);
else if (b < 1024.0 * 1024.0)
snprintf(out, outsz, "%.0f kB/s", b / 1024.0);
else
snprintf(out, outsz, "%.1f MB/s", b / (1024.0 * 1024.0));
}
static void fmt_eta(double seconds, char *out, size_t outsz)
{
if (seconds < 0 || seconds > 99 * 3600) {
snprintf(out, outsz, "--:--");
return;
}
long s = (long)(seconds + 0.5);
snprintf(out, outsz, "%02ld:%02ld", s / 60, s % 60);
}
void progress_init(progress *p, const char *label, long long total)
{
memset(p, 0, sizeof(*p));
p->mode = progress_mode_from_env();
if (p->mode == PROG_AUTO) {
/* auto: barra solo su terminale, silenzio se rediretto/in pipeline */
p->mode = isatty(STDOUT_FILENO) ? PROG_BAR : PROG_OFF;
}
p->label = label ? label : "";
p->total = total;
p->last_percent = -1;
p->plain_step = 0;
p->width = terminal_width();
p->unicode = env_is_utf8();
p->t_start = now_monotonic();
p->t_last_draw = -1.0;
p->t_last_sample = p->t_start;
if (getenv("CELLAR_PROGRESS_DEBUG"))
fprintf(stderr, "[debug progress] mode=%d width=%d unicode=%d isatty=%d\n", (int)p->mode,
p->width, p->unicode, isatty(STDOUT_FILENO));
if (p->mode == PROG_PLAIN)
printf("%s: avvio...\n", p->label);
}
void progress_set_members(progress *p, long long members)
{
p->members = members;
}
static void build_line(progress *p, char *line, size_t linesz, int final)
{
char done_s[32], total_s[32], speed_s[32], eta_s[16];
fmt_size(p->done, done_s, sizeof(done_s));
if (p->total > 0)
fmt_size(p->total, total_s, sizeof(total_s));
else
snprintf(total_s, sizeof(total_s), "?");
int percent = -1;
if (p->total > 0) {
percent = (int)((p->done * 100) / p->total);
if (percent > 100)
percent = 100;
}
fmt_speed(p->rate, speed_s, sizeof(speed_s));
double eta = -1.0;
if (p->total > 0 && p->rate > 4096.0)
eta = (double)(p->total - p->done) / p->rate;
fmt_eta(eta, eta_s, sizeof(eta_s));
char members_s[48];
members_s[0] = '\0';
if (p->members > 0)
snprintf(members_s, sizeof(members_s), " membri: %lld", p->members);
/* suffisso senza barra */
char suffix[192];
if (percent >= 0)
snprintf(suffix, sizeof(suffix), "] %3d%% %s/%s %s%s", percent, done_s, total_s, speed_s,
members_s);
else
snprintf(suffix, sizeof(suffix), "] %s %s%s", done_s, speed_s, members_s);
if (!final && p->rate <= 4096.0) {
/* senza stima affidabile non mostriamo ETA */
} else if (eta >= 0) {
char tmp[224];
snprintf(tmp, sizeof(tmp), "%s ETA %s", suffix, eta_s);
snprintf(suffix, sizeof(suffix), "%s", tmp);
}
if (p->mode == PROG_PLAIN) {
/* log-friendly: niente barra grafica, una riga per soglia */
if (percent >= 0)
snprintf(line, linesz, "%s: %d%% %s/%s %s%s", p->label, percent, done_s, total_s,
speed_s, members_s);
else
snprintf(line, linesz, "%s: %s %s%s", p->label, done_s, speed_s, members_s);
return;
}
char prefix[96];
snprintf(prefix, sizeof(prefix), "%s [", p->label);
size_t avail = 0;
size_t plen = strlen(prefix), slen = strlen(suffix);
if (p->width > (int)(plen + slen))
avail = (size_t)p->width - plen - slen;
if (avail < 8) {
/* troppo stretta per la barra: riduce il suffisso */
snprintf(line, linesz, "%s%s", prefix, suffix);
return;
}
size_t filled = 0;
if (p->total > 0)
filled = (size_t)((p->done * (long long)avail) / p->total);
if (filled > avail)
filled = avail;
size_t used = 0;
int n = snprintf(line, linesz, "%s", prefix);
if (n > 0)
used = (size_t)n;
const char *full = p->unicode ? PROG_UNI_FULL : NULL;
const char *empty = p->unicode ? PROG_UNI_EMPTY : NULL;
for (size_t i = 0; i < avail && used + 8 < linesz; i++) {
if (i < filled) {
if (full)
used += (size_t)snprintf(line + used, linesz - used, "%s", full);
else
line[used++] = PROG_ASCII_FULL;
} else {
if (empty)
used += (size_t)snprintf(line + used, linesz - used, "%s", empty);
else
line[used++] = PROG_ASCII_EMPTY;
}
}
line[used] = '\0';
snprintf(line + used, linesz - used, "%s", suffix);
}
static void draw_line(progress *p, const char *line)
{
/* attenzione: i glifi della barra sono multi-byte in UTF-8, quindi il
* conteggio per il ridisegno e la pulizia va fatto in COLONNE (code point),
* non in byte, altrimenti la riga supera la larghezza del terminale. */
size_t cols = utf8_count(line);
if (getenv("CELLAR_PROGRESS_DEBUG"))
fprintf(stderr, "[debug progress] line bytes=%zu cols=%zu\n", strlen(line), cols);
fputs("\r", stdout);
fputs(line, stdout);
for (size_t i = cols; i < (size_t)p->last_cols; i++)
fputc(' ', stdout);
p->last_cols = (int)cols;
fflush(stdout);
}
void progress_update(progress *p, long long done)
{
if (p->mode == PROG_OFF || p->finished)
return;
p->done = done;
double now = now_monotonic();
if (now - p->t_last_sample >= PROG_SAMPLE_INTERVAL) {
double dt = now - p->t_last_sample;
long long delta = done - p->last_sample_done;
if (dt > 0 && delta >= 0) {
double inst = (double)delta / dt;
p->rate = (p->rate > 0.0) ? (0.7 * p->rate + 0.3 * inst) : inst;
}
p->last_sample_done = done;
p->t_last_sample = now;
}
if (p->mode == PROG_PLAIN) {
int percent = (p->total > 0) ? (int)((done * 100) / p->total) : 0;
if (p->total > 0) {
if (percent >= p->plain_step) {
char line[512];
build_line(p, line, sizeof(line), 0);
printf("%s\n", line);
p->plain_step += 10;
}
} else if (now - p->t_last_draw >= 3.0) {
char line[512];
build_line(p, line, sizeof(line), 0);
printf("%s\n", line);
p->t_last_draw = now;
}
return;
}
/* PROG_BAR */
int percent = (p->total > 0) ? (int)((done * 100) / p->total) : -1;
if (p->t_last_draw > 0.0 && now - p->t_last_draw < PROG_DRAW_INTERVAL)
return;
if (percent == p->last_percent && p->total > 0 && done != p->total)
return;
p->last_percent = percent;
p->t_last_draw = now;
char line[512];
build_line(p, line, sizeof(line), 0);
draw_line(p, line);
}
void progress_finish(progress *p)
{
if (p->mode == PROG_OFF || p->finished)
return;
p->finished = 1;
if (p->total > 0)
p->done = p->total;
double elapsed = now_monotonic() - p->t_start;
if (elapsed <= 0.0)
elapsed = 0.001;
double avg = (double)p->done / elapsed;
char done_s[32], speed_s[32];
fmt_size(p->done, done_s, sizeof(done_s));
fmt_speed(avg, speed_s, sizeof(speed_s));
if (p->mode == PROG_PLAIN) {
printf("%s: completato (%s in %.1f s, %s)\n", p->label, done_s, elapsed, speed_s);
return;
}
char line[512];
build_line(p, line, sizeof(line), 1);
draw_line(p, line);
printf("\n%s: %s in %.1f s (%s)\n", p->label, done_s, elapsed, speed_s);
}
void progress_abort(progress *p)
{
if (p->mode == PROG_OFF || p->finished)
return;
p->finished = 1;
if (p->mode == PROG_BAR && p->last_cols > 0)
printf("\n");
}
+52
View File
@@ -0,0 +1,52 @@
/* progress.h — barra di avanzamento per download ed estrazione.
*
* Attiva solo quando ha senso: con CELLAR_PROGRESS=auto (default) disegna la
* barra se stdout e' un terminale, e non stampa nulla se l'output e' rediretto
* o in pipeline (cosi' l'output resta identico al client Python e i log
* restano puliti). Modalita' forzabili: bar, plain, off.
*
* Nessun codice ANSI: solo '\r' e riempimento con spazi, quindi funziona anche
* su console vecchie/seriali. Glyph ASCII di default, Unicode solo se la
* locale e' UTF-8.
*/
#ifndef CELLAR_PROGRESS_H
#define CELLAR_PROGRESS_H
#include "common.h"
#include <stddef.h>
typedef enum { PROG_AUTO = 0, PROG_BAR, PROG_PLAIN, PROG_OFF } prog_mode;
typedef struct {
prog_mode mode;
const char *label;
long long total; /* -1 se sconosciuto */
long long done;
long long members; /* informazione opzionale (estrazione) */
double t_start;
double t_last_draw;
double t_last_sample;
double rate; /* byte/s, media mobile */
long long last_sample_done;
int last_percent;
int last_cols; /* larghezza (colonne) dell'ultima riga disegnata, per ripulire */
int plain_step; /* prossima soglia in modalita' plain */
int width;
int unicode;
int finished;
} progress;
/* Modalita' da CELLAR_PROGRESS (auto|bar|plain|off); default auto. */
prog_mode progress_mode_from_env(void);
/* Inizializza: in modalita' AUTO decide in base a isatty(stdout). */
void progress_init(progress *p, const char *label, long long total);
void progress_set_members(progress *p, long long members);
/* Aggiorna con i byte completati (throttled: non ridisegna piu' di ~8 volte/s). */
void progress_update(progress *p, long long done);
/* Chiude la riga (newline) e stampa il riepilogo finale. */
void progress_finish(progress *p);
/* Percorso d'errore: chiude solo la riga, senza riepilogo. */
void progress_abort(progress *p);
#endif /* CELLAR_PROGRESS_H */
+22 -1
View File
@@ -374,6 +374,9 @@ int tar_create(const char *archive_path, const char *rootpath, const char *arcna
typedef struct {
gz_reader *gz;
long long remaining; /* byte non ancora letti del membro corrente */
tar_progress_fn cb; /* opzionale: avanzamento */
void *cb_ctx;
long long members;
} tr;
static int tr_read_exact(tr *t, void *buf, size_t len, cerror *e)
@@ -389,6 +392,14 @@ static int tr_read_exact(tr *t, void *buf, size_t len, cerror *e)
return -1;
}
off += (size_t)n;
if (t->cb) {
long long in_bytes = 0;
gzr_progress(t->gz, &in_bytes, NULL);
if (t->cb(t->cb_ctx, in_bytes, t->members) != 0) {
err_set(e, ERR_MISC, 0, "operazione annullata");
return -1;
}
}
}
return 0;
}
@@ -542,7 +553,7 @@ static int apply_times(const char *path, time_t sec, long nsec, int symlink_ok)
return utimensat(AT_FDCWD, path, times, 0);
}
int tar_extract(const char *archive_path, const char *destdir, cerror *e)
int tar_extract_cb(const char *archive_path, const char *destdir, tar_progress_fn cb, void *ctx, cerror *e)
{
gz_reader *gz = gzr_open(archive_path, e);
if (!gz)
@@ -550,6 +561,9 @@ int tar_extract(const char *archive_path, const char *destdir, cerror *e)
tr t;
t.gz = gz;
t.remaining = 0;
t.cb = cb;
t.cb_ctx = ctx;
t.members = 0;
dirfixup *dirs = NULL;
size_t ndirs = 0, cdirs = 0;
@@ -854,6 +868,8 @@ int tar_extract(const char *archive_path, const char *destdir, cerror *e)
}
}
t.members++;
/* gli override valgono per un solo membro */
pax_free(&next);
free(gnu_longname);
@@ -879,3 +895,8 @@ int tar_extract(const char *archive_path, const char *destdir, cerror *e)
gzr_close(gz);
return rc;
}
int tar_extract(const char *archive_path, const char *destdir, cerror *e)
{
return tar_extract_cb(archive_path, destdir, NULL, NULL, e);
}
+5
View File
@@ -16,4 +16,9 @@ int tar_create(const char *archive_path, const char *rootpath, const char *arcna
* (symlink/hardlink preservati, permessi e mtime ripristinati). */
int tar_extract(const char *archive_path, const char *destdir, cerror *e);
/* Come tar_extract, ma notifica l'avanzamento: byte compressi consumati e
* membri gia' processati. Il callback puo' ritornare -1 per interrompere. */
typedef int (*tar_progress_fn)(void *ctx, long long consumed, long long members);
int tar_extract_cb(const char *archive_path, const char *destdir, tar_progress_fn cb, void *ctx, cerror *e);
#endif /* CELLAR_TAR_H */
+32 -4
View File
@@ -19,6 +19,7 @@ import json
import os
import re
import threading
import time
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
@@ -75,8 +76,14 @@ def parse_multipart(body: bytes, content_type: str):
class Handler(BaseHTTPRequestHandler):
state: State
throttle = 0 # byte/s, 0 = nessun limite (per testare la barra di avanzamento)
protocol_version = "HTTP/1.1"
@classmethod
def _pace(cls, nbytes: int) -> None:
if cls.throttle > 0:
time.sleep(nbytes / cls.throttle)
def log_message(self, *args): # silenzioso
pass
@@ -90,7 +97,20 @@ class Handler(BaseHTTPRequestHandler):
def _body(self) -> bytes:
length = int(self.headers.get("Content-Length") or 0)
return self.rfile.read(length) if length else b""
if not length:
return b""
if Handler.throttle <= 0:
return self.rfile.read(length)
chunks, left = [], length
while left > 0:
n = min(left, 65536)
data = self.rfile.read(n)
if not data:
break
chunks.append(data)
left -= len(data)
Handler._pace(len(data))
return b"".join(chunks)
def do_GET(self):
path = self.path.split("?")[0]
@@ -113,13 +133,19 @@ class Handler(BaseHTTPRequestHandler):
f = self.state.storage / r["stored_name"]
if not f.exists():
return self._json(404, {"detail": "Stored file not found."})
data = f.read_bytes()
self.send_response(200)
self.send_header("Content-Type", r.get("content_type") or "application/octet-stream")
self.send_header("Content-Disposition", f'attachment; filename="{r["file_name"]}"')
self.send_header("Content-Length", str(len(data)))
self.send_header("Content-Length", str(f.stat().st_size))
self.end_headers()
return self.wfile.write(data)
with f.open("rb") as fh:
while True:
chunk = fh.read(65536)
if not chunk:
break
self.wfile.write(chunk)
Handler._pace(len(chunk))
return None
return self._json(404, {"detail": "Archive not found."})
if path == "/openapi.json":
return self._json(200, {"info": {"title": "Bottle Archive Server", "version": "0.1.0"}})
@@ -181,8 +207,10 @@ def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=18099)
ap.add_argument("--state", required=True)
ap.add_argument("--throttle", type=int, default=0, help="byte/s massimi (per testare la barra)")
args = ap.parse_args()
Handler.state = State(Path(args.state))
Handler.throttle = args.throttle
srv = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
srv.serve_forever()
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
# Test della barra di avanzamento (src/progress.c).
#
# Verifica:
# 1. modalita' auto con output rediretto -> NESSUN output di progresso
# (cosi' l'output resta identico al client Python: vedi parity_test.sh);
# 2. modalita' auto su terminale (PTY via `script`) -> barra disegnata per il
# download e per l'estrazione, con riga di riepilogo finale;
# 3. CELLAR_PROGRESS=plain -> una riga per soglia del 10%, niente barra;
# 4. CELLAR_PROGRESS=off -> nessun output di progresso anche su PTY;
# 5. l'output finale (Downloaded to / Installed bottle) resta corretto e
# l'archivio scaricato e' integro.
set -uo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
C_BIN="${C_BIN:-$ROOT/dist/cellar-cli}"
PY="${PYTHON:-python3}"
WORK="$ROOT/tests/tmp-progress"
PORT="${PORT:-18097}"
THROTTLE="${THROTTLE:-3000000}" # 3 MB/s: la barra disegna piu' fotogrammi
SRV_PID=""
PASS=0
FAIL=0
ok() { PASS=$((PASS + 1)); printf ' PASS %s\n' "$1"; }
ko() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n' "$1"; }
cleanup() {
[ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null
wait 2>/dev/null
return 0
}
trap cleanup EXIT
fail_if_missing() {
[ -x "$C_BIN" ] || { echo "binario non trovato: $C_BIN (esegui make all)"; exit 1; }
command -v script >/dev/null || { echo "serve util-linux 'script' per il test su PTY"; exit 1; }
}
fail_if_missing
rm -rf "$WORK"
mkdir -p "$WORK"/{state,inst,bottles,home}
# bottiglia di prova con un file da ~12 MB (comprimibile poco, cosi' il
# download dura qualche secondo con il throttling)
bash "$ROOT/tests/make_fake_bottle.sh" "$WORK/bottles" >/dev/null
head -c 12000000 /dev/urandom >"$WORK/bottles/Fake Bottle/drive_c/big.bin"
"$PY" "$ROOT/tests/mock_server.py" --port "$PORT" --state "$WORK/state" --throttle "$THROTTLE" &
SRV_PID=$!
for _ in $(seq 60); do
curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1 && break
sleep 0.1
done
SRV="http://127.0.0.1:$PORT"
export HOME="$WORK/home"
# archivio tar.gz valido (l'estrazione deve poter funzionare)
"$PY" - "$WORK/bottles/Fake Bottle" "$WORK/big.tar.gz" <<'PYEOF'
import sys, tarfile
with tarfile.open(sys.argv[2], "w:gz") as tf:
tf.add(sys.argv[1], arcname="Fake Bottle")
PYEOF
"$C_BIN" --server "$SRV" upload "$WORK/big.tar.gz" --name BigBottle \
--bottle-name "Fake Bottle" >/dev/null 2>&1
echo " (server mock pronto, archivio valido da $(stat -c%s "$WORK/big.tar.gz") byte caricato)"
# ---------------------------------------------------------------- 1. auto su pipe
out=$(CELLAR_PROGRESS=auto "$C_BIN" --server "$SRV" download 1 "$WORK/dl-pipe.bin" 2>&1)
if [ "$out" = "Downloaded to $WORK/dl-pipe.bin" ]; then
ok "auto su pipe: nessun output di progresso"
else
ko "auto su pipe: nessun output di progresso (ottenuto: $(printf '%s' "$out" | head -2 | tr '\n' '|'))"
fi
# ------------------------------------------------------------ 2. auto su PTY (barra)
pty_out=$(script -qec "CELLAR_PROGRESS=auto '$C_BIN' --server '$SRV' install 'Fake Bottle' --bottles-dir '$WORK/inst'" /dev/null 2>&1 | tr '\r' '\n')
if grep -q "Scaricamento \[" <<<"$pty_out"; then
ok "PTY: barra del download disegnata"
else
ko "PTY: barra del download disegnata"
fi
if grep -q "Estrazione \[" <<<"$pty_out" || grep -q "^Estrazione:" <<<"$pty_out"; then
ok "PTY: fase di estrazione tracciata"
else
ko "PTY: fase di estrazione tracciata"
fi
if grep -qE "^Scaricamento: [0-9.]+ (B|kB|MB|GB) in [0-9.]+ s" <<<"$pty_out"; then
ok "PTY: riepilogo finale del download"
else
ko "PTY: riepilogo finale del download"
fi
if grep -q "^Installed bottle 'Fake Bottle' to " <<<"$pty_out"; then
ok "PTY: output finale corretto dopo la barra"
else
ko "PTY: output finale corretto dopo la barra"
fi
if grep -q "ETA" <<<"$pty_out"; then
ok "PTY: ETA mostrata durante il trasferimento"
else
ko "PTY: ETA mostrata durante il trasferimento"
fi
# nessun codice ANSI (retro-compatibilita' delle console)
if grep -qP '\x1b\[' <<<"$pty_out"; then
ko "PTY: nessun codice ANSI nel disegno"
else
ok "PTY: nessun codice ANSI nel disegno"
fi
# la barra non deve eccedere la larghezza del terminale (script: 80 colonne).
# I glifi Unicode sono multi-byte: si contano i code point, non i byte.
# solo le righe disegnate dalla barra (l'output normale puo' essere lungo)
bar_lines=$(grep -E "^(Scaricamento|Estrazione)[ :]" <<<"$pty_out")
longest=$(printf '%s' "$bar_lines" | "$PY" -c "import sys; print(max((len(l) for l in sys.stdin.read().split('\\n')), default=0))")
if [ "${longest:-0}" -le 80 ]; then
ok "PTY: righe entro 80 colonne (max $longest)"
else
ko "PTY: righe entro 80 colonne (max $longest)"
printf '%s' "$bar_lines" | "$PY" -c "
import sys
lines = sys.stdin.buffer.read().decode('utf-8', 'replace').split('\n')
for n, l in sorted(((len(x), x) for x in lines), reverse=True)[:3]:
print(f' {n:3} |{l}|')
"
fi
# --------------------------------------------------------------- 3. modalita' plain
plain_out=$(CELLAR_PROGRESS=plain "$C_BIN" --server "$SRV" download 1 "$WORK/dl-plain.bin" 2>&1)
lines=$(grep -c "^Scaricamento: [0-9]" <<<"$plain_out")
if [ "$lines" -ge 10 ]; then
ok "plain: $lines righe di avanzamento (>=10)"
else
ko "plain: righe di avanzamento insufficienti ($lines)"
fi
if grep -q "^Scaricamento: completato" <<<"$plain_out" && ! grep -q "\[" <<<"$plain_out"; then
ok "plain: riepilogo presente e nessuna barra grafica"
else
ko "plain: riepilogo presente e nessuna barra grafica"
fi
# ---------------------------------------------------------------------- 4. off
off_out=$(script -qec "CELLAR_PROGRESS=off '$C_BIN' --server '$SRV' download 1 '$WORK/dl-off.bin'" /dev/null 2>&1 | tr '\r' '\n')
if ! grep -qE "Scaricamento" <<<"$off_out"; then
ok "off: nessun output di progresso (anche su PTY)"
else
ko "off: nessun output di progresso (anche su PTY)"
fi
# ------------------------------------------------- 5. integrita' dell'artefatto
if [ -f "$WORK/dl-pipe.bin" ] && cmp -s "$WORK/dl-pipe.bin" "$WORK/big.tar.gz"; then
ok "integrita': il file scaricato coincide con quello caricato"
else
ko "integrita': il file scaricato coincide con quello caricato"
fi
if [ -f "$WORK/inst/Fake Bottle/bottle.yml" ]; then
ok "install: bottiglia installata nonostante il disegno della barra"
else
ko "install: bottiglia installata nonostante il disegno della barra"
fi
echo
echo "=================================================="
printf 'PASS: %d FAIL: %d\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ] || exit 1
echo "barra di avanzamento ok ✔"
Binary file not shown.
@@ -0,0 +1,5 @@
Name: 'Fake Bottle'
Runner: soda-9.0-1
Arch: win32
Windows: win10
Environment: Gaming
@@ -0,0 +1 @@
../drive_c/config.ini
@@ -0,0 +1 @@
hello world
@@ -0,0 +1 @@
/nonexistent/target
@@ -0,0 +1 @@
config
@@ -0,0 +1 @@
config.ini
@@ -0,0 +1 @@
unicode
+1
View File
@@ -0,0 +1 @@
binario
@@ -0,0 +1 @@
deep
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
[cellar]
server = http://127.0.0.1:8080
bottles_dir = /home/enne2/Dev/cellar-cli-c/tests/tmp-progress/home/.var/app/com.usebottles.bottles/data/bottles/bottles
@@ -0,0 +1,5 @@
Name: 'Fake Bottle'
Runner: soda-9.0-1
Arch: win32
Windows: win10
Environment: Gaming
@@ -0,0 +1 @@
../drive_c/config.ini
@@ -0,0 +1 @@
hello world
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
/nonexistent/target
@@ -0,0 +1 @@
config
@@ -0,0 +1 @@
config.ini
@@ -0,0 +1 @@
unicode
+1
View File
@@ -0,0 +1 @@
binario
@@ -0,0 +1 @@
deep
+18
View File
@@ -0,0 +1,18 @@
[
{
"name": "BigBottle",
"bottle_name": "Fake Bottle",
"description": null,
"tags": null,
"arch": null,
"runner": null,
"windows_version": null,
"id": 1,
"file_name": "big.tar.gz",
"stored_name": "b2b18dc0668e8b5b661c952112aeb58a.tar.gz",
"content_type": "application/gzip",
"size_bytes": 12009363,
"sha256": "b2b18dc0668e8b5b661c952112aeb58a3b16a1552156b0098823e47ca89f2434",
"created_at": "2026-09-20T15:44:25.190979+00:00"
}
]