Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f82187c67 | ||
|
|
6f5b224b2f |
@@ -50,7 +50,8 @@ verify: all
|
|||||||
tests/verify_binary.sh $(DIST)/$(NAME)
|
tests/verify_binary.sh $(DIST)/$(NAME)
|
||||||
|
|
||||||
test: asan
|
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:
|
clean:
|
||||||
rm -rf $(DIST) build tests/tmp
|
rm -rf $(DIST) build tests/tmp
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ Autore: Matteo Benedetto — progetto derivato da `enne2/cellar` (commit `f5216b
|
|||||||
| Build | `x86_64` static, `i686` static, `aarch64` static (cross) |
|
| Build | `x86_64` static, `i686` static, `aarch64` static (cross) |
|
||||||
| Installazione locale | `~/.local/bin/cellar-cli` → symlink a `dist/cellar-cli` |
|
| 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
|
## Build
|
||||||
|
|
||||||
```bash
|
```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).
|
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)
|
## Retrocompatibilità (le scelte che contano)
|
||||||
|
|
||||||
- **Binario statico** (`-static`): nessuna dipendenza da glibc a runtime, quindi gira su
|
- **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 |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 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
|
## Test e verifica
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test # parità (usa la build ASan)
|
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/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
|
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
|
## 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/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/ui.{c,h} tabelle e JSON con larghezze in code point (come le f-string)
|
||||||
src/ops.{c,h} list/upload/download/install
|
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/wizard.{c,h} flussi interattivi
|
||||||
src/main.c CLI e messaggi di errore in stile argparse
|
src/main.c CLI e messaggi di errore in stile argparse
|
||||||
third_party/ miniz (unlicense/MIT) + licenza
|
third_party/ miniz (unlicense/MIT) + licenza
|
||||||
|
|||||||
@@ -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
@@ -30,6 +30,7 @@ struct gz_reader {
|
|||||||
mz_stream zs;
|
mz_stream zs;
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
char in[GZ_BUFSZ];
|
char in[GZ_BUFSZ];
|
||||||
|
unsigned long long total_in; /* byte compressi consumati */
|
||||||
int stream_end;
|
int stream_end;
|
||||||
unsigned long crc;
|
unsigned long crc;
|
||||||
unsigned long long total_out;
|
unsigned long long total_out;
|
||||||
@@ -247,6 +248,11 @@ gz_reader *gzr_open(const char *path, cerror *e)
|
|||||||
free(r);
|
free(r);
|
||||||
return NULL;
|
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 */
|
int rc = mz_inflateInit2(&r->zs, -15); /* deflate raw */
|
||||||
if (rc != MZ_OK) {
|
if (rc != MZ_OK) {
|
||||||
err_set(e, ERR_MISC, 0, "inflate init failed (%d)", rc);
|
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 */
|
break; /* EOF inatteso: lo segnala il chiamante */
|
||||||
}
|
}
|
||||||
|
r->total_in += (unsigned long long)n;
|
||||||
r->zs.next_in = (unsigned char *)r->in;
|
r->zs.next_in = (unsigned char *)r->in;
|
||||||
r->zs.avail_in = (unsigned int)n;
|
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;
|
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)
|
void gzr_close(gz_reader *r)
|
||||||
{
|
{
|
||||||
if (!r)
|
if (!r)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ int gzw_close(gz_writer *w, cerror *e);
|
|||||||
gz_reader *gzr_open(const char *path, cerror *e);
|
gz_reader *gzr_open(const char *path, cerror *e);
|
||||||
/* Ritorna il numero di byte letti, 0 a EOF, -1 su errore. */
|
/* 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);
|
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);
|
void gzr_close(gz_reader *r);
|
||||||
|
|
||||||
#endif /* CELLAR_GZIP_H */
|
#endif /* CELLAR_GZIP_H */
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "fsutil.h"
|
#include "fsutil.h"
|
||||||
#include "http.h"
|
#include "http.h"
|
||||||
#include "json.h"
|
#include "json.h"
|
||||||
|
#include "progress.h"
|
||||||
#include "tar.h"
|
#include "tar.h"
|
||||||
#include "ui.h"
|
#include "ui.h"
|
||||||
|
|
||||||
@@ -141,6 +142,7 @@ int ops_upload(const char *server, const char *filepath, const mp_field *fields,
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
FILE *fp;
|
FILE *fp;
|
||||||
long long written;
|
long long written;
|
||||||
|
progress *pr; /* opzionale: barra di avanzamento */
|
||||||
} file_sink;
|
} file_sink;
|
||||||
|
|
||||||
static int sink_write(void *ctx, const char *buf, size_t len, cerror *e)
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
s->written += (long long)len;
|
s->written += (long long)len;
|
||||||
|
if (s->pr)
|
||||||
|
progress_update(s->pr, s->written);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,9 +191,12 @@ static int download_to(const char *server, long long archive_id, const char *des
|
|||||||
csresponse_free(&meta);
|
csresponse_free(&meta);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
progress pr;
|
||||||
|
progress_init(&pr, "Scaricamento", meta.content_length);
|
||||||
file_sink sink;
|
file_sink sink;
|
||||||
sink.fp = fp;
|
sink.fp = fp;
|
||||||
sink.written = 0;
|
sink.written = 0;
|
||||||
|
sink.pr = ≺
|
||||||
int rc = http_read_body(c, sink_write, &sink, NULL, e);
|
int rc = http_read_body(c, sink_write, &sink, NULL, e);
|
||||||
if (fclose(fp) != 0 && rc == 0) {
|
if (fclose(fp) != 0 && rc == 0) {
|
||||||
err_set_errno(e, ERR_FILE, "cannot write %s", dest);
|
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);
|
http_close(c);
|
||||||
csresponse_free(&meta);
|
csresponse_free(&meta);
|
||||||
if (rc != 0)
|
if (rc != 0) {
|
||||||
return -1;
|
progress_abort(&pr);
|
||||||
|
return -1; /* come il client Python: il file parziale resta */
|
||||||
|
}
|
||||||
|
progress_finish(&pr);
|
||||||
if (announce)
|
if (announce)
|
||||||
printf("Downloaded to %s\n", dest);
|
printf("Downloaded to %s\n", dest);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -252,9 +262,12 @@ int ops_download(const char *server, long long archive_id, const char *output, c
|
|||||||
csresponse_free(&meta);
|
csresponse_free(&meta);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
progress pr;
|
||||||
|
progress_init(&pr, "Scaricamento", meta.content_length);
|
||||||
file_sink sink;
|
file_sink sink;
|
||||||
sink.fp = fp;
|
sink.fp = fp;
|
||||||
sink.written = 0;
|
sink.written = 0;
|
||||||
|
sink.pr = ≺
|
||||||
int rc = http_read_body(c, sink_write, &sink, NULL, e);
|
int rc = http_read_body(c, sink_write, &sink, NULL, e);
|
||||||
if (fclose(fp) != 0 && rc == 0) {
|
if (fclose(fp) != 0 && rc == 0) {
|
||||||
err_set_errno(e, ERR_FILE, "cannot write %s", dest);
|
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);
|
http_close(c);
|
||||||
csresponse_free(&meta);
|
csresponse_free(&meta);
|
||||||
if (rc != 0)
|
if (rc != 0) {
|
||||||
|
progress_abort(&pr);
|
||||||
return -1;
|
return -1;
|
||||||
|
}
|
||||||
|
progress_finish(&pr);
|
||||||
printf("Downloaded to %s\n", dest);
|
printf("Downloaded to %s\n", dest);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return download_to(server, archive_id, output, 1, e);
|
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,
|
int ops_install_record(const char *server, const archive_rec *rec, const char *bottles_dir, int replace,
|
||||||
const char *ref, cerror *e)
|
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)
|
if (mkdir_p(extract_dir, e) != 0)
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
if (tar_extract(download_path, extract_dir, e) != 0)
|
{
|
||||||
|
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;
|
goto cleanup;
|
||||||
|
}
|
||||||
|
progress_finish(&px);
|
||||||
|
}
|
||||||
|
|
||||||
strlist subdirs;
|
strlist subdirs;
|
||||||
strlist_init(&subdirs);
|
strlist_init(&subdirs);
|
||||||
|
|||||||
+336
@@ -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");
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -374,6 +374,9 @@ int tar_create(const char *archive_path, const char *rootpath, const char *arcna
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
gz_reader *gz;
|
gz_reader *gz;
|
||||||
long long remaining; /* byte non ancora letti del membro corrente */
|
long long remaining; /* byte non ancora letti del membro corrente */
|
||||||
|
tar_progress_fn cb; /* opzionale: avanzamento */
|
||||||
|
void *cb_ctx;
|
||||||
|
long long members;
|
||||||
} tr;
|
} tr;
|
||||||
|
|
||||||
static int tr_read_exact(tr *t, void *buf, size_t len, cerror *e)
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
off += (size_t)n;
|
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;
|
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);
|
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);
|
gz_reader *gz = gzr_open(archive_path, e);
|
||||||
if (!gz)
|
if (!gz)
|
||||||
@@ -550,6 +561,9 @@ int tar_extract(const char *archive_path, const char *destdir, cerror *e)
|
|||||||
tr t;
|
tr t;
|
||||||
t.gz = gz;
|
t.gz = gz;
|
||||||
t.remaining = 0;
|
t.remaining = 0;
|
||||||
|
t.cb = cb;
|
||||||
|
t.cb_ctx = ctx;
|
||||||
|
t.members = 0;
|
||||||
|
|
||||||
dirfixup *dirs = NULL;
|
dirfixup *dirs = NULL;
|
||||||
size_t ndirs = 0, cdirs = 0;
|
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 */
|
/* gli override valgono per un solo membro */
|
||||||
pax_free(&next);
|
pax_free(&next);
|
||||||
free(gnu_longname);
|
free(gnu_longname);
|
||||||
@@ -879,3 +895,8 @@ int tar_extract(const char *archive_path, const char *destdir, cerror *e)
|
|||||||
gzr_close(gz);
|
gzr_close(gz);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int tar_extract(const char *archive_path, const char *destdir, cerror *e)
|
||||||
|
{
|
||||||
|
return tar_extract_cb(archive_path, destdir, NULL, NULL, e);
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,4 +16,9 @@ int tar_create(const char *archive_path, const char *rootpath, const char *arcna
|
|||||||
* (symlink/hardlink preservati, permessi e mtime ripristinati). */
|
* (symlink/hardlink preservati, permessi e mtime ripristinati). */
|
||||||
int tar_extract(const char *archive_path, const char *destdir, cerror *e);
|
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 */
|
#endif /* CELLAR_TAR_H */
|
||||||
|
|||||||
+32
-4
@@ -19,6 +19,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -75,8 +76,14 @@ def parse_multipart(body: bytes, content_type: str):
|
|||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
state: State
|
state: State
|
||||||
|
throttle = 0 # byte/s, 0 = nessun limite (per testare la barra di avanzamento)
|
||||||
protocol_version = "HTTP/1.1"
|
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
|
def log_message(self, *args): # silenzioso
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -90,7 +97,20 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def _body(self) -> bytes:
|
def _body(self) -> bytes:
|
||||||
length = int(self.headers.get("Content-Length") or 0)
|
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):
|
def do_GET(self):
|
||||||
path = self.path.split("?")[0]
|
path = self.path.split("?")[0]
|
||||||
@@ -113,13 +133,19 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
f = self.state.storage / r["stored_name"]
|
f = self.state.storage / r["stored_name"]
|
||||||
if not f.exists():
|
if not f.exists():
|
||||||
return self._json(404, {"detail": "Stored file not found."})
|
return self._json(404, {"detail": "Stored file not found."})
|
||||||
data = f.read_bytes()
|
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", r.get("content_type") or "application/octet-stream")
|
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-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()
|
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."})
|
return self._json(404, {"detail": "Archive not found."})
|
||||||
if path == "/openapi.json":
|
if path == "/openapi.json":
|
||||||
return self._json(200, {"info": {"title": "Bottle Archive Server", "version": "0.1.0"}})
|
return self._json(200, {"info": {"title": "Bottle Archive Server", "version": "0.1.0"}})
|
||||||
@@ -181,8 +207,10 @@ def main():
|
|||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--port", type=int, default=18099)
|
ap.add_argument("--port", type=int, default=18099)
|
||||||
ap.add_argument("--state", required=True)
|
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()
|
args = ap.parse_args()
|
||||||
Handler.state = State(Path(args.state))
|
Handler.state = State(Path(args.state))
|
||||||
|
Handler.throttle = args.throttle
|
||||||
srv = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
|
srv = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
|
||||||
srv.serve_forever()
|
srv.serve_forever()
|
||||||
|
|
||||||
|
|||||||
Executable
+165
@@ -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
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
long path
|
||||||
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
binario
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
deep
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
long path
|
||||||
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
@@ -0,0 +1 @@
|
|||||||
|
binario
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
deep
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
Binary file not shown.
Reference in New Issue
Block a user