#include #include #include #include #include #include #include #include #include #include using Json = nlohmann::json; struct Response { std::string body, etag; }; size_t receive(char* bytes, size_t size, size_t count, void* target) { static_cast(target)->append(bytes, size * count); return size * count; } size_t header(char* bytes, size_t size, size_t count, void* target) { std::string line(bytes, size * count), name = line.substr(0, 5); std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); }); if (name == "etag:") { auto value = line.substr(5); auto first = value.find_first_not_of(" \t\r\n\""); auto last = value.find_last_not_of(" \t\r\n\""); *static_cast(target) = first == std::string::npos ? "" : value.substr(first, last - first + 1); } return size * count; } Response request(const std::string& url, const char* method, const std::string& body, const std::string& key = "", bool json = false, bool createOnly = false) { CURL* curl = curl_easy_init(); if (!curl) throw std::runtime_error("Cannot initialize HTTP client"); Response response; curl_slist* headers = nullptr; if (!key.empty()) headers = curl_slist_append(headers, ("Authorization: Bearer " + key).c_str()); headers = curl_slist_append(headers, json ? "Content-Type: application/json" : "Content-Type: application/octet-stream"); if (createOnly) headers = curl_slist_append(headers, "If-None-Match: *"); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.data()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(body.size())); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 300L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receive); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response.etag); auto error = curl_easy_perform(curl); long status = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (error != CURLE_OK) throw std::runtime_error(curl_easy_strerror(error)); if (status < 200 || status >= 300) throw std::runtime_error("HTTP " + std::to_string(status) + ": " + response.body); return response; } int main(int argc, char** argv) { if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1; int result = 0; try { const char* token = std::getenv("STASHBAY_API_KEY"); if (!token || argc < 2) throw std::runtime_error("Set STASHBAY_API_KEY and run: ./upload FILE"); std::string key(token), origin = std::getenv("STASHBAY_ORIGIN") ? std::getenv("STASHBAY_ORIGIN") : "https://stashbay.net"; if (!origin.empty() && origin.back() == '/') origin.pop_back(); auto api = [&](const std::string& route, const Json& body) { return Json::parse(request(origin + "/api/v1" + route, "POST", body.dump(), key, true).body); }; std::filesystem::path path(argv[1]); auto size = std::filesystem::file_size(path); std::ifstream file(path, std::ios::binary); if (!file) throw std::runtime_error("Cannot open file"); auto plan = api("/uploads/init", {{"filename", path.filename().string()}, {"sizeBytes", size}}); std::string fileId = plan.at("fileId"), strategy = plan.at("strategy"); std::cerr << "Upload ID: " << fileId << '\n'; auto partSize = plan.at("partSize").get(); if (partSize < 1 || partSize > 64 * 1024 * 1024) throw std::runtime_error("Unexpected part size"); bool single = plan.at("single"); Json parts = Json::array(); for (int number = 1; number <= plan.at("partCount").get(); number++) { auto length = std::min(partSize, size - (number - 1) * partSize); std::string bytes(length, '\0'); file.read(bytes.data(), static_cast(length)); if (static_cast(file.gcount()) != length) throw std::runtime_error("File changed during upload"); std::string address = single ? plan.at("uploadUrl").get() : api("/uploads/parts", {{"fileId", fileId}, {"from", number}, {"count", 1}}).at("parts").at(0).at("url").get(); std::string url, auth; if (strategy == "proxy" && address.rfind("/api/v1/uploads/", 0) == 0) { url = origin + address; auth = key; } else if (strategy == "presigned" && address.rfind("https://", 0) == 0) { url = address; } else throw std::runtime_error("Unexpected upload URL or strategy"); auto response = request(url, "PUT", bytes, auth, false, single && strategy == "presigned"); if (!single) { std::string etag = strategy == "proxy" ? Json::parse(response.body).at("etag").get() : response.etag; if (etag.empty()) throw std::runtime_error("Missing part ETag"); etag.erase(std::remove(etag.begin(), etag.end(), '"'), etag.end()); parts.push_back({{"partNumber", number}, {"etag", etag}}); } } auto complete = api("/uploads/complete", {{"fileId", fileId}, {"parts", parts}}); std::cout << (complete.at("shareUrl").is_string() ? complete.at("shareUrl").get() : complete.dump()) << '\n'; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; } curl_global_cleanup(); return result; }