Public Access
For carrying on a download that broke (issue #54): the head's ETag and Content-Range are read, a 206 is checked to be the rest of the same file, and a policy says how many tries a download is worth. Host-tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
54 lines
2.3 KiB
C++
54 lines
2.3 KiB
C++
#pragma once
|
|
|
|
#include <string>
|
|
|
|
#include "http_head.h"
|
|
|
|
namespace roro::release {
|
|
|
|
// Carrying on a download that broke (issue #54): the connection is opened again with
|
|
// "Range: bytes=<from>-", and what was already received stays where it is. The Update File's own
|
|
// hash and signature are what decide whether the whole is good; these checks only keep the device
|
|
// from carrying on with something that plainly is not the rest of the same file.
|
|
|
|
// The validator to send as "If-Range", so a file that changed comes back whole (a 200) instead of
|
|
// in part: only a strong ETag may be used for that. "" for none.
|
|
inline std::string ifRangeFor(const std::string& etag) {
|
|
return etag.size() >= 2 && etag.front() == '"' && etag.back() == '"' ? etag : "";
|
|
}
|
|
|
|
// Is this answer the rest of the file, from byte `from` of `total`? "" if so, or why not, in
|
|
// words for the screen. `etag` is what the first answer gave ("" if nothing).
|
|
inline std::string resumeRefusal(const HttpHead& head, long from, long total, const std::string& etag) {
|
|
if (head.status == 200) return "The file changed on the server";
|
|
if (head.status != 206) return "The server answered " + std::to_string(head.status);
|
|
if (head.chunked || head.rangeFrom != from) return "The server sent another part of the file";
|
|
if (head.rangeTotal != total || head.rangeTo != total - 1 || head.contentLength != total - from) return "The file changed size on the server";
|
|
if (!etag.empty() && !head.etag.empty() && head.etag != etag) return "The file changed on the server";
|
|
return "";
|
|
}
|
|
|
|
// How long to keep trying. A break that came after new bytes is the link being poor, and is
|
|
// worth another try; breaks with nothing in between mean it is not coming back.
|
|
class ResumePolicy {
|
|
public:
|
|
static constexpr int kMaxResumes = 20; // in one download
|
|
static constexpr int kMaxStuck = 3; // in a row without a byte
|
|
|
|
// The connection broke with `received` bytes of the file here: try again?
|
|
bool again(long received) {
|
|
stuck_ = received > last_ ? 0 : stuck_ + 1;
|
|
last_ = received;
|
|
if (resumes_ >= kMaxResumes || stuck_ >= kMaxStuck) return false;
|
|
resumes_++;
|
|
return true;
|
|
}
|
|
int resumes() const { return resumes_; }
|
|
|
|
private:
|
|
long last_ = 0;
|
|
int resumes_ = 0, stuck_ = 0;
|
|
};
|
|
|
|
} // namespace roro::release
|