add alipan
This commit is contained in:
346
src/drive/alipan.cpp
Normal file
346
src/drive/alipan.cpp
Normal file
@@ -0,0 +1,346 @@
|
||||
#include "alipan.h"
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
|
||||
#include <mbedtls/ecdsa.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <mbedtls/base64.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
using json = nlohmann::json;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const std::string ALIPAN_GET_QRCODE =
|
||||
"https://passport.alipan.com/newlogin/qrcode/generate.do?appName=aliyun_drive&fromSite=52";
|
||||
const std::string ALIPAN_CHECK_QRCODE =
|
||||
"https://passport.alipan.com/newlogin/qrcode/query.do?appName=aliyun_drive&fromSite=52";
|
||||
const std::string ALIPAN_PRE_AUTH =
|
||||
"https://auth.alipan.com/v2/oauth/authorize?login_type=custom&response_type=code"
|
||||
"&redirect_uri=https%3A%2F%2Fwww.aliyundrive.com%2Fsign%2Fcallback&client_id=25dzX3vbYqktVxyX"
|
||||
"&sid=ls2z86hbnehde&state=%7B%22origin%22%3A%22file%3A%2F%2F%22%7D";
|
||||
const std::string ALIPAN_MINI_LOGIN =
|
||||
"https://passport.alipan.com/mini_login.htm?lang=zh_cn&appName=aliyun_drive&appEntrance=web"
|
||||
"&styleType=auto&bizParams=¬LoadSsoView=false¬KeepLogin=false&isMobile=false";
|
||||
const std::string ALIPAN_CANARY = "X-Canary: client=windows,app=adrive,version=v4.12.0";
|
||||
|
||||
bool alipan::qrLogin(fnQrcode printQr) {
|
||||
try {
|
||||
std::ifstream f(".alipan.json");
|
||||
if (f.is_open()) {
|
||||
json j = json::parse(f);
|
||||
this->access_token = j.at("access_token");
|
||||
this->refresh_token = j.at("refresh_token");
|
||||
this->device_id = j.at("device_id");
|
||||
}
|
||||
|
||||
if (!this->access_token.empty()) {
|
||||
return this->getSelfuser();
|
||||
} else if (!this->refresh_token.empty()) {
|
||||
return this->refreshToken();
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
fmt::print("load token {}\n", ex.what());
|
||||
}
|
||||
|
||||
HTTP s;
|
||||
std::stringstream ss;
|
||||
|
||||
s.set_headers({
|
||||
"Origin: https://passport.alipan.com",
|
||||
"Referer: " + ALIPAN_MINI_LOGIN,
|
||||
ALIPAN_CANARY,
|
||||
});
|
||||
s.get(ALIPAN_GET_QRCODE, &ss);
|
||||
json r = json::parse(ss.str());
|
||||
json qr = r.at("content").at("data");
|
||||
if (qr == nullptr || !qr.contains("codeContent")) return -1;
|
||||
printQr(qr.at("codeContent"));
|
||||
|
||||
std::string query = HTTP::encode_form({
|
||||
{"t", std::to_string(qr.at("t").get<time_t>())},
|
||||
{"ck", qr.at("ck")},
|
||||
{"appName", "aliyun_drive"},
|
||||
{"appEntrance", "web"},
|
||||
{"isMobile", "false"},
|
||||
{"lang", "zh_CN"},
|
||||
{"returnUrl", ""},
|
||||
{"fromSite", "52"},
|
||||
{"bizParams", ""},
|
||||
});
|
||||
|
||||
s.set_headers({
|
||||
"Content-Type: application/x-www-form-urlencoded",
|
||||
"Origin: https://passport.alipan.com",
|
||||
"Referer: " + ALIPAN_MINI_LOGIN,
|
||||
ALIPAN_CANARY,
|
||||
});
|
||||
|
||||
bool scaned = false;
|
||||
while (true) {
|
||||
json j = json::parse(s.post(ALIPAN_CHECK_QRCODE, query));
|
||||
json data = j.at("content").at("data");
|
||||
std::string qrCodeStatus = data.at("qrCodeStatus");
|
||||
|
||||
if (qrCodeStatus == "NEW") {
|
||||
} else if (qrCodeStatus == "SCANED") {
|
||||
if (!scaned) {
|
||||
fmt::print("扫描成功 请在APP上确认\n");
|
||||
scaned = true;
|
||||
}
|
||||
} else if (qrCodeStatus == "EXPIRED") {
|
||||
fmt::print("二维码过期\n");
|
||||
return false;
|
||||
} else if (qrCodeStatus == "CANCELED") {
|
||||
fmt::print("已取消\n");
|
||||
return false;
|
||||
} else if (qrCodeStatus == "CONFIRMED") {
|
||||
// decode token
|
||||
std::string bizExt = data.at("bizExt");
|
||||
std::vector<char> gbkBiz(bizExt.size());
|
||||
size_t out_len = 0;
|
||||
|
||||
mbedtls_base64_decode(
|
||||
(uint8_t*)gbkBiz.data(), gbkBiz.size(), &out_len, (const uint8_t*)bizExt.c_str(), bizExt.size());
|
||||
|
||||
static std::regex remove_re("[^\\x20-\\x7f]");
|
||||
bizExt = std::regex_replace(std::string(gbkBiz.data(), out_len), remove_re, "");
|
||||
if (bizExt.empty()) return -1;
|
||||
|
||||
json login = json::parse(bizExt).at("pds_login_result");
|
||||
this->user_id = login.at("userId");
|
||||
this->drive_id = login.at("defaultDriveId");
|
||||
this->access_token = login.at("accessToken");
|
||||
this->refresh_token = login.at("refreshToken");
|
||||
// 计算设备ID
|
||||
unsigned char digest[32];
|
||||
mbedtls_sha256_ret((uint8_t*)this->user_id.c_str(), this->user_id.size(), digest, 0);
|
||||
this->device_id = hex_encode(digest, sizeof(digest));
|
||||
fmt::print("login user_id({}) drive_id({})\n", this->user_id, this->drive_id);
|
||||
|
||||
return this->createSession();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<dItem> alipan::list(const std::string& file_id) {
|
||||
json data = {
|
||||
{"drive_id", this->drive_id},
|
||||
{"parent_file_id", file_id},
|
||||
{"limit", 20},
|
||||
{"all", false},
|
||||
{"url_expire_sec", 14400},
|
||||
{"image_thumbnail_process", "image/resize,w_256/format,jpeg"},
|
||||
{"image_url_process", "image/resize,w_1920/format,jpeg/interlace,1"},
|
||||
{"video_thumbnail_process", "video/snapshot,t_1000,f_jpg,ar_auto,w_256"},
|
||||
{"fields", "file_id,name,type,"},
|
||||
{"order_by", "updated_at"},
|
||||
{"order_direction", "DESC"},
|
||||
};
|
||||
json j = this->request("/adrive/v3/file/list", data);
|
||||
|
||||
std::vector<dItem> list;
|
||||
for (auto& item : j.at("items")) {
|
||||
dItem it;
|
||||
it.folder = item.at("type") == "folder";
|
||||
it.id = item.at("file_id");
|
||||
it.name = item.at("name");
|
||||
list.push_back(it);
|
||||
}
|
||||
|
||||
fmt::print("{}\n", j.dump(2));
|
||||
return list;
|
||||
}
|
||||
|
||||
std::string alipan::link(const std::string& file_id) {
|
||||
json data = {
|
||||
{"drive_id", this->drive_id},
|
||||
{"file_id", file_id},
|
||||
{"expire_sec", 14400},
|
||||
};
|
||||
json j = this->request("/v2/file/get_download_url", data);
|
||||
return j.at("url");
|
||||
}
|
||||
|
||||
std::string alipan::mkdir(const std::string& parent_id, const std::string& name) {
|
||||
json data = {
|
||||
{"drive_id", this->drive_id},
|
||||
{"parent_file_id", parent_id},
|
||||
{"name", name},
|
||||
{"type", "folder"},
|
||||
{"check_name_mode", "refuse"},
|
||||
};
|
||||
json j = this->request("/adrive/v2/file/createWithFolders", data);
|
||||
return j.at("file_id");
|
||||
}
|
||||
|
||||
void alipan::remove(const std::string& file_id) {
|
||||
json data = {
|
||||
{"drive_id", this->drive_id},
|
||||
{"file_id", file_id},
|
||||
};
|
||||
this->request("/v2/recyclebin/trash", data);
|
||||
}
|
||||
|
||||
std::string alipan::upload(const std::string& parent_id, const std::string& file) {
|
||||
fs::path name(file);
|
||||
json data = {
|
||||
{"drive_id", this->drive_id},
|
||||
{
|
||||
"part_info_list",
|
||||
{
|
||||
{{"part_number", 1}},
|
||||
},
|
||||
},
|
||||
{"parent_file_id", parent_id},
|
||||
{"name", name.filename().string()},
|
||||
{"size", fs::file_size(file)},
|
||||
{"type", "file"},
|
||||
{"check_name_mode", "overwrite"},
|
||||
{"create_scene", "file_upload"},
|
||||
{"device_name", device_name()},
|
||||
{"content_hash_name", "none"},
|
||||
{"proof_version", "v1"},
|
||||
};
|
||||
json j = this->request("/adrive/v2/file/createWithFolders", data);
|
||||
|
||||
std::ifstream f(file, std::ios::binary);
|
||||
HTTP c;
|
||||
for (auto& info : j.at("part_info_list")) {
|
||||
std::string resp = c.put(info.at("upload_url"), &f);
|
||||
if (!resp.empty()) fmt::print("upload part: {}\n", resp);
|
||||
}
|
||||
|
||||
data = {
|
||||
{"drive_id", this->drive_id},
|
||||
{"file_id", j.at("file_id")},
|
||||
{"upload_id", j.at("upload_id")},
|
||||
};
|
||||
j = this->request("/v2/file/complete", data);
|
||||
return j.at("file_id");
|
||||
}
|
||||
|
||||
bool alipan::getSelfuser() {
|
||||
json j = this->request("/v2/user/get", {});
|
||||
this->user_id = j.at("user_id");
|
||||
this->drive_id = j.at("default_drive_id");
|
||||
return this->createSession();
|
||||
}
|
||||
|
||||
json alipan::request(const std::string& api, const json& data) {
|
||||
HTTP s;
|
||||
while (true) {
|
||||
std::vector<std::string> headers = {
|
||||
"Content-Type: application/json",
|
||||
ALIPAN_CANARY,
|
||||
};
|
||||
if (this->access_token.size() > 0) {
|
||||
headers.push_back("Authorization: Bearer " + this->access_token);
|
||||
}
|
||||
if (this->device_id.size() > 0) {
|
||||
headers.push_back("X-Device-Id: " + this->device_id);
|
||||
}
|
||||
if (this->signature.size() > 0) {
|
||||
headers.push_back("X-Signature: " + this->signature);
|
||||
}
|
||||
s.set_headers(headers);
|
||||
json j = json::parse(s.post("https://api.alipan.com" + api, data.dump()));
|
||||
|
||||
if (!j.contains("code")) return j;
|
||||
if (j.at("code").is_null()) return j;
|
||||
|
||||
std::string code = j.at("code");
|
||||
if (code == "AccessTokenInvalid") {
|
||||
if (this->refresh_token.empty()) throw std::runtime_error(j.at("message"));
|
||||
this->refreshToken();
|
||||
} else if (code == "DeviceSessionSignatureInvalid") {
|
||||
this->createSession();
|
||||
} else {
|
||||
throw std::runtime_error(j.at("message"));
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool alipan::refreshToken() {
|
||||
HTTP s;
|
||||
s.set_headers({"Content-Type: application/json", "x-requested-with: XMLHttpRequest"});
|
||||
json data = {{"refresh_token", this->refresh_token}, {"grant_type", "refresh_token"}};
|
||||
std::string r = s.post("https://auth.alipan.com/v2/account/token", data.dump());
|
||||
json j = json::parse(r);
|
||||
if (j.contains("code")) {
|
||||
fmt::print("refresh failed: {}\n", j.dump());
|
||||
return -1;
|
||||
}
|
||||
|
||||
this->refresh_token = j.at("refresh_token");
|
||||
this->access_token = j.at("access_token");
|
||||
|
||||
fmt::print("refress success ({})\n", this->refresh_token);
|
||||
return this->createSession();
|
||||
}
|
||||
|
||||
static inline int mbd_rand(void* rng_state, unsigned char* output, size_t len) {
|
||||
srand(time(nullptr));
|
||||
for (size_t i = 0; i < len; ++i) output[i] = rand();
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool alipan::createSession() {
|
||||
std::string msg = "5dde4e1bdf9e4966b387ba58f4b3fdc3:" + this->device_id + ":" + this->user_id + ":0";
|
||||
std::string pub_key;
|
||||
|
||||
mbedtls_ecdsa_context ctx_sign;
|
||||
mbedtls_ecdsa_init(&ctx_sign);
|
||||
// gen secp256k1 keypair
|
||||
mbedtls_ecdsa_genkey(&ctx_sign, MBEDTLS_ECP_DP_SECP256K1, mbd_rand, nullptr);
|
||||
// mbedtls_ecp_group_load(&ctx_sign.grp, MBEDTLS_ECP_DP_SECP256K1);
|
||||
// mbedtls_mpi_read_string(&ctx_sign.d, 16, this->device_id.c_str());
|
||||
// mbedtls_ecp_mul(&ctx_sign.grp, &ctx_sign.Q, &ctx_sign.d, &ctx_sign.grp.G, mbd_rand, nullptr);
|
||||
|
||||
// dump public key
|
||||
size_t pub_len = 0;
|
||||
std::vector<uint8_t> pub(MBEDTLS_ECP_MAX_BYTES, 0);
|
||||
mbedtls_ecp_point_write_binary(
|
||||
&ctx_sign.grp, &ctx_sign.Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &pub_len, pub.data(), pub.size());
|
||||
pub_key = hex_encode(pub.data(), pub_len);
|
||||
|
||||
// sign message
|
||||
unsigned char msg_hash[32];
|
||||
mbedtls_sha256_ret((uint8_t*)msg.c_str(), msg.size(), msg_hash, 0);
|
||||
|
||||
mbedtls_mpi r, s;
|
||||
std::vector<uint8_t> sigdata(MBEDTLS_ECDSA_MAX_LEN, 0);
|
||||
mbedtls_mpi_init(&r);
|
||||
mbedtls_mpi_init(&s);
|
||||
mbedtls_ecdsa_sign(&ctx_sign.grp, &r, &s, &ctx_sign.d, msg_hash, sizeof(msg_hash), mbd_rand, nullptr);
|
||||
|
||||
size_t plen = mbedtls_mpi_size(&r);
|
||||
mbedtls_mpi_write_binary(&r, sigdata.data(), plen);
|
||||
mbedtls_mpi_write_binary(&s, sigdata.data() + plen, plen);
|
||||
sigdata[plen * 2] = 1;
|
||||
mbedtls_mpi_free(&r);
|
||||
mbedtls_mpi_free(&s);
|
||||
mbedtls_ecdsa_free(&ctx_sign);
|
||||
this->signature = hex_encode(sigdata.data(), plen * 2 + 1);
|
||||
// log_debug("sign ({}), pubkey ({}), msg ({})", this->signature, pub_key, msg);
|
||||
|
||||
json data = {
|
||||
{"deviceName", device_name()},
|
||||
{"modelName", "Windows客户端"},
|
||||
{"pubKey", pub_key},
|
||||
};
|
||||
json j = this->request("/users/v1/users/device/create_session", data);
|
||||
|
||||
std::ofstream(".alipan.json") << json({
|
||||
{"access_token", this->access_token},
|
||||
{"refresh_token", this->refresh_token},
|
||||
{"device_id", this->device_id},
|
||||
});
|
||||
return j.at("success").get<bool>();
|
||||
}
|
||||
31
src/drive/alipan.h
Normal file
31
src/drive/alipan.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "http.h"
|
||||
#include "drive.h"
|
||||
|
||||
class alipan : public drive {
|
||||
public:
|
||||
bool qrLogin(fnQrcode printQr) override;
|
||||
std::vector<dItem> list(const std::string& file_id) override;
|
||||
std::string link(const std::string& file_id) override;
|
||||
std::string mkdir(const std::string& parent_id, const std::string& name) override;
|
||||
void remove(const std::string& file_id) override;
|
||||
std::string upload(const std::string& parent_id, const std::string& file) override;
|
||||
|
||||
private:
|
||||
bool getSelfuser();
|
||||
bool refreshToken();
|
||||
bool createSession();
|
||||
|
||||
nlohmann::json request(const std::string& api, const nlohmann::json& j);
|
||||
|
||||
private:
|
||||
std::string device_id;
|
||||
std::string signature;
|
||||
std::string access_token;
|
||||
std::string refresh_token;
|
||||
|
||||
std::string drive_id;
|
||||
std::string user_id;
|
||||
};
|
||||
18
src/drive/drive.cpp
Normal file
18
src/drive/drive.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "alipan.h"
|
||||
|
||||
static std::map<drive_type, drive::ref> m;
|
||||
|
||||
drive::ref new_drive(drive_type type) {
|
||||
auto it = m.find(type);
|
||||
if (it == m.end()) {
|
||||
switch (type) {
|
||||
case dt_alipan:
|
||||
it->second = std::make_shared<alipan>();
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unsupport drive type");
|
||||
}
|
||||
m.insert(std::make_pair(type, it->second));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
69
src/drive/misc.cpp
Normal file
69
src/drive/misc.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#include "drive.h"
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
#elif defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <SystemConfiguration/SystemConfiguration.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
std::string hex_encode(const unsigned char* data, size_t len) {
|
||||
std::stringstream ss;
|
||||
for (size_t i = 0; i < len; i++) ss << std::hex << std::setw(2) << std::setfill('0') << (int)data[i];
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string ansi_to_utf8(const char* locale, std::string const& str_gbk) {
|
||||
std::vector<wchar_t> buff(str_gbk.size());
|
||||
std::locale loc(locale);
|
||||
wchar_t* to_next = nullptr;
|
||||
const char* from_next = nullptr;
|
||||
mbstate_t state = {};
|
||||
int res = std::use_facet<std::codecvt<wchar_t, char, mbstate_t>>(loc).in(state, str_gbk.data(),
|
||||
str_gbk.data() + str_gbk.size(), from_next, buff.data(), buff.data() + buff.size(), to_next);
|
||||
|
||||
if (std::codecvt_base::ok != res) return "";
|
||||
|
||||
std::wstring_convert<std::codecvt_utf8<wchar_t>> cutf8;
|
||||
return cutf8.to_bytes(std::wstring(buff.data(), to_next));
|
||||
}
|
||||
|
||||
std::string device_name() {
|
||||
#ifdef __SWITCH__
|
||||
SetSysDeviceNickName nick;
|
||||
if (R_SUCCEEDED(setsysGetDeviceNickname(&nick))) {
|
||||
return nick.nickname;
|
||||
}
|
||||
#elif defined(_WIN32)
|
||||
DWORD nSize = 128;
|
||||
std::vector<WCHAR> buf(nSize);
|
||||
if (GetComputerNameW(buf.data(), &nSize)) {
|
||||
std::string name;
|
||||
name.resize(nSize);
|
||||
WideCharToMultiByte(CP_UTF8, 0, buf.data(), nSize, name.data(), name.size(), nullptr, nullptr);
|
||||
return name;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
CFStringRef nameRef = SCDynamicStoreCopyComputerName(nullptr, nullptr);
|
||||
if (nameRef) {
|
||||
std::vector<char> name(CFStringGetLength(nameRef) * 3);
|
||||
CFStringGetCString(nameRef, name.data(), name.size(), kCFStringEncodingUTF8);
|
||||
CFRelease(nameRef);
|
||||
return name.data();
|
||||
}
|
||||
#else
|
||||
std::vector<char> buf(128);
|
||||
if (gethostname(buf.data(), buf.size()) == 0) {
|
||||
return buf.data();
|
||||
}
|
||||
#endif
|
||||
return "clist";
|
||||
}
|
||||
@@ -86,6 +86,7 @@ std::string HTTP::encode_form(const Form& form) {
|
||||
}
|
||||
|
||||
int HTTP::get(const std::string& url, std::ostream* out) {
|
||||
out->clear();
|
||||
curl_easy_setopt(this->easy, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(this->easy, CURLOPT_HTTPGET, 1L);
|
||||
return this->perform(out);
|
||||
|
||||
50
src/main.cpp
50
src/main.cpp
@@ -1,5 +1,7 @@
|
||||
#include "http.h"
|
||||
#include "drive.h"
|
||||
#include <qrcodegen/qrcodegen.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
void printQr(const std::string& text) {
|
||||
int border = 1;
|
||||
@@ -10,16 +12,58 @@ void printQr(const std::string& text) {
|
||||
|
||||
for (int y = -border; y < width + border; y++) {
|
||||
for (int x = -border; x < width + border; x++) {
|
||||
qrcodegen_getModule(qrcode.data(), x, y) ? printf("\033[40m \033[0m") : printf("\033[47m \033[0m");
|
||||
qrcodegen_getModule(qrcode.data(), x, y) ? fmt::print("\033[40m \033[0m")
|
||||
: fmt::print("\033[47m \033[0m");
|
||||
}
|
||||
printf("\n");
|
||||
fmt::print("\n");
|
||||
}
|
||||
printf("\n");
|
||||
fmt::print("\n");
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
curl_global_init(CURL_GLOBAL_ALL);
|
||||
|
||||
drive::ref c = new_drive(dt_alipan);
|
||||
if (!c->qrLogin(printQr)) return 1;
|
||||
|
||||
uint32_t choice = 0;
|
||||
std::vector<std::string> stack = {"root"};
|
||||
while (true) {
|
||||
auto list = c->list(stack.back());
|
||||
if (stack.size() > 1) {
|
||||
fmt::print("0: 返回上一级\n");
|
||||
}
|
||||
for (size_t i = 0; i < list.size(); i++) {
|
||||
fmt::print("{}: {}\n", i + 1, list[i].name);
|
||||
}
|
||||
fmt::print("{}: 创建目录\n", list.size() + 1);
|
||||
fmt::print("{}: 上传文件\n", list.size() + 2);
|
||||
fmt::print("选择文件或目录: \n");
|
||||
scanf("%u", &choice);
|
||||
|
||||
if (choice > list.size() + 2 || choice < 0) {
|
||||
break;
|
||||
} else if (choice == 0 && stack.size() > 1) {
|
||||
stack.pop_back();
|
||||
} else if (choice == list.size() + 1) {
|
||||
char name[1024];
|
||||
fmt::print("请输入目录名: \n");
|
||||
scanf("%s", name);
|
||||
c->mkdir(stack.back(), name);
|
||||
} else if (choice == list.size() + 2) {
|
||||
char name[1024];
|
||||
fmt::print("请输入文件名: \n");
|
||||
scanf("%s", name);
|
||||
c->upload(stack.back(), name);
|
||||
} else if (list[choice - 1].folder) {
|
||||
stack.push_back(list[choice - 1].id);
|
||||
} else {
|
||||
std::string link = c->link(list[choice - 1].id);
|
||||
fmt::print("url: {}\n", link);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
curl_global_cleanup();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user