From d75ed23e7253b5987d073e229e3233ea4ad2e95b Mon Sep 17 00:00:00 2001 From: BreakZer0 Date: Wed, 25 Mar 2026 20:54:16 +0100 Subject: [PATCH 1/5] chore: add .DS_Store to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0cbe95a..cff817b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.elf *.exe fetchpkg +.DS_Store From f2085acc06c71a6df03b056e3e92bb294e0c119b Mon Sep 17 00:00:00 2001 From: BreakZer0 Date: Wed, 25 Mar 2026 20:54:25 +0100 Subject: [PATCH 2/5] feat: add download resume support Check for existing file on disk and skip already-downloaded pieces based on file size. Open in append mode when resuming. Track session-only bytes for accurate speed and ETA calculation. --- dl.c | 23 ++++++++++++++++++++--- main.c | 7 +++++-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/dl.c b/dl.c index 6fe02e7..cb4e5ef 100644 --- a/dl.c +++ b/dl.c @@ -293,6 +293,8 @@ dl_package(const char* manifest_url, const char* path, dl_progress_t* cb, const char* hash; const char* url; int error = 0; + size_t existing = 0; + FILE *f; state.on_progress.cb = cb; state.on_progress.ctx = ctx; @@ -306,9 +308,17 @@ dl_package(const char* manifest_url, const char* path, dl_progress_t* cb, fprintf(stderr, "dl_package: malformed manifest\n"); error = -1; - } else if(!(state.file=fopen(path, "wb"))) { - fprintf(stderr, "dl_package: %s\n", strerror(errno)); - error = -1; + } else { + if ((f = fopen(path, "rb"))) { + fseeko(f, 0, SEEK_END); + existing = ftello(f); + fclose(f); + } + + if (!(state.file = fopen(path, existing > 0 ? "ab" : "wb"))) { + fprintf(stderr, "dl_package: %s\n", strerror(errno)); + error = -1; + } } state.remaining = json_object_get_number(manifest, "originalFileSize"); @@ -316,6 +326,13 @@ dl_package(const char* manifest_url, const char* path, dl_progress_t* cb, piece = json_array_get_object(pieces, i); url = json_object_get_string(piece, "url"); hash = json_object_get_string(piece, "hashValue"); + size_t piece_size = (size_t)json_object_get_number(piece, "fileSize"); + + if (existing >= piece_size) { + existing -= piece_size; + state.remaining -= piece_size; + continue; + } memset(expected_hash, 0, sizeof(expected_hash)); memset(actual_hash, 0, sizeof(actual_hash)); diff --git a/main.c b/main.c index af49c62..9742c92 100644 --- a/main.c +++ b/main.c @@ -64,18 +64,21 @@ endswith(const char *str, const char* suffix) { static int on_progress(void* ctx, size_t written, size_t remaining) { const char* filename = (const char*)ctx; + static size_t start_written = 0; static time_t start_time = 0; static time_t prev_time = 0; time_t now = time(0); if(start_time == 0) { start_time = prev_time = now; + start_written = written; } if(prev_time < now) { double progress = 100.0 * written / (written + remaining); - double speed = written / (now - start_time); - int eta = remaining / speed; + double elapsed = now - start_time; + double speed = elapsed > 0 ? (written - start_written) / elapsed : 0; + int eta = speed > 0 ? remaining / speed : 0; int h = eta / 3600; int m = (eta % 3600) / 60; int s = eta % 60; From 4e261d89a2eb509fb9d2c13ccf4042a27130b700 Mon Sep 17 00:00:00 2001 From: BreakZer0 Date: Wed, 25 Mar 2026 21:31:45 +0100 Subject: [PATCH 3/5] fix: truncate partial piece before resuming download When a download is interrupted mid-piece, the partial data remains in the file. On resume, seek back and truncate to the last complete piece boundary so the piece is re-downloaded cleanly. --- dl.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dl.c b/dl.c index cb4e5ef..046d22a 100644 --- a/dl.c +++ b/dl.c @@ -334,6 +334,13 @@ dl_package(const char* manifest_url, const char* path, dl_progress_t* cb, continue; } + if (existing > 0) { + fseeko(state.file, -existing, SEEK_END); + ftruncate(fileno(state.file), ftello(state.file)); + state.remaining += existing; + existing = 0; + } + memset(expected_hash, 0, sizeof(expected_hash)); memset(actual_hash, 0, sizeof(actual_hash)); hex2bin(hash, expected_hash, sizeof(expected_hash)); From 77ef70dbf41682d30208ec83b9a13f2e6670ef46 Mon Sep 17 00:00:00 2001 From: BreakZer0 Date: Wed, 25 Mar 2026 23:54:55 +0100 Subject: [PATCH 4/5] feat: add in-session retry with range resume on connection loss Detect connection hangs via CURLOPT_LOW_SPEED_LIMIT (1000 bytes/sec for 30s). On failure, retry the piece from the last byte offset using HTTP range requests, up to 20 attempts with 30s delay between each. The SHA context stays alive across retries so hash verification remains valid. --- dl.c | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/dl.c b/dl.c index 046d22a..51e5fe5 100644 --- a/dl.c +++ b/dl.c @@ -18,6 +18,7 @@ along with this program; see the file COPYING. If not, see #include #include #include +#include #include @@ -147,7 +148,7 @@ dl_package_write(void *ptr, size_t length, size_t nmemb, void *ctx) { * **/ static int -dl_fetch(const char* url, dl_data_write_t* cb, void* ctx) { +dl_fetch(const char* url, off_t offset, dl_data_write_t* cb, void* ctx) { char buf[CURL_ERROR_SIZE]; struct curl_blob ca = {0}; const char* proxy; @@ -172,6 +173,15 @@ dl_fetch(const char* url, dl_data_write_t* cb, void* ctx) { curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, buf); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1000L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 30L); + + if(offset > 0) { + char range[64]; + snprintf(range, sizeof(range), "%lld-", (long long)offset); + curl_easy_setopt(curl, CURLOPT_RANGE, range); + fprintf(stderr, "\nResuming piece at byte offset %lld\n", (long long)offset); + } if((proxy=getenv("CURL_PROXY"))) { curl_easy_setopt(curl, CURLOPT_PROXY, proxy); @@ -197,7 +207,7 @@ dl_manifest(const char *url) { dl_manifest_state_t state = {0}; JSON_Value *json = 0; - if(!dl_fetch(url, dl_manifest_write, &state)) { + if(!dl_fetch(url, 0, dl_manifest_write, &state)) { if(state.error) { fprintf(stderr, "dl_manifest: %s\n", strerror(state.error)); } else if(!(json=json_parse_string(state.data))) { @@ -221,6 +231,10 @@ dl_package_piece(dl_package_state_t* state, const char *url, uint8_t* hash, size_t hash_size) { SHA256_CTX sha256; SHA1_CTX sha1; + off_t start_pos = ftello(state->file); + off_t offset = 0; + int retries = 0; + int max_retries = 20; state->sha1 = 0; state->sha256 = 0; @@ -233,8 +247,25 @@ dl_package_piece(dl_package_state_t* state, const char *url, uint8_t* hash, state->sha1 = &sha1; } - if(dl_fetch(url, dl_package_write, state)) { - return -1; + while(retries <= max_retries) { + if(!dl_fetch(url, offset, dl_package_write, state)) { + break; + } + + state->error = 0; + offset = ftello(state->file) - start_pos; + retries++; + + fprintf(stderr, "\nConnection lost after %lld bytes into piece (attempt %d/%d)\n", + (long long)offset, retries, max_retries); + + if(retries > max_retries) { + fprintf(stderr, "Max retries reached, giving up on piece\n"); + return -1; + } + + fprintf(stderr, "Retrying in 30 seconds...\n"); + sleep(30); } if(state->error) { From 3eafdc1870572ff21e1c232f6ca85dcc7e49eee6 Mon Sep 17 00:00:00 2001 From: BreakZer0 Date: Wed, 25 Mar 2026 23:54:59 +0100 Subject: [PATCH 5/5] chore: add build/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cff817b..8a71ea3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.exe fetchpkg .DS_Store +build/