그저 내가 개발중인 게임과 툴에서 OneDrive에 억세스 하고 싶었다. C++에서 C#코드를 부를 생각은 없었다. 네이티브 C++로 하고 싶었다.
예전에 Casablanca(C++ Rest SDK)를 사용해서 OneDrivePlayerW81이란 앱을 만든 적이 있다. 그때 Casablanca 쓰면서 엄청 짜증났던 기억이 있다. 그래서 이번엔 Casablanca를 쓰지 않고 UWP API와 C++/CX로 구현할 생각이다.
일단 UWP 앱이지만 데스크탑앱에서도 UWP의 HttpClient를 사용할 수 있으므로 추후 데스크탑 포팅도 가능하지 않을까 기대해본다.(확실히 가능한지는 아직 모른다).
하여간 몇 일전 시작했고 현재까지 진행상황이다.
api.onedrive.com/v1.0으로 시작해서 어느 정도 뼈대를 잡았는데 자동 로그인 처리를 하려니 문제가 많아서 포기.
그래서 apis.live.net/v5.0 로 돌아왔다. 몇일 삽질 해둔 덕에 REST get,post체계는 만들어놔서 비교적 쉽게 바꿨다.
co_await를 적극 사용해보고 싶었지만 이게 아직 실험적인 기능이라 HttpClient의 GetAsync()따위의 메소드에는 사용 불가. 결국 빌어먹을 create_task().then().then()의 task 체인 지옥을 피해갈 수 없었다.
api.onedrive.com/v1.0에 대한 토큰을 REST 호출로 받아오려니 이게 좀 문제가 있다. 자동 로그인을 처리할 방법이 막막하다. refresh_token을 사용해서 처리해보려고 별 짓을 다 해봤는데 내가 뭘 잘못했는지 refresh_token처리에 계속 실패하고 있다. 이게 제대로 작동을 해도 웹에서나 어울리지 앱에서 사용하기엔 영 불편해서 결국 포기했다.
apis.live.net/v5.0 에 Windows::Security::Authentication::OnlineId::OnlineIdAuthenticator를 사용하면 토큰 받아오는 절차가 매우 간편할 뿐더러 자동로그인도 그야말로 자동으로 된다. 로그인 팝업을 띄울 필요가 없다.
로그인 코드는 다음과 같다. 토큰을 받아오자마자 루트 폴더의 내용을 가져온다.
String^ scope = L"wl.signin wl.basic wl.photos wl.skydrive_update";
auto request = ref new OnlineIdServiceTicketRequest(scope, "DELEGATION");
m_Authenticator = ref new Windows::Security::Authentication::OnlineId::OnlineIdAuthenticator();
auto login_task = create_task(m_Authenticator->AuthenticateUserAsync(request));
login_task.then([this](task<Windows::Security::Authentication::OnlineId::UserIdentity^> ident_task)
{
bool success = false;
UserIdentity^ identity = nullptr;
try
{
identity = ident_task.get();
if (identity)
{
if (identity->Tickets->Size)
{
success = true;
}
}
}
catch (Exception^ e)
{
const WCHAR* errMsg = e->Message->Data();
OutputDebugString(errMsg);
}
if (!success)
return;
auto ticket = identity->Tickets->GetAt(0);
String^ token = ticket->Value;
HttpClient^ httpClient = ref new HttpClient();
String^ cmd = L"https://apis.live.net/v5.0/me?access_token=" + token;
Uri^ uri = ref new Uri(cmd);
auto task_folder = create_task(httpClient->GetStringAsync(uri));
task_folder.then([this,token](task<String^> task_json)
{
String^ json = nullptr;
try
{
json = task_json.get();
}
catch (Exception^ e)
{
const WCHAR* err = e->Message->Data();
OutputDebugString(err);
}
if (!json)
{
return;
}
String^ UserName = GetUserName(json);
});
});
String^ token = ticket->Value;에서 받아온 토큰은 저장해둔다. 이후에는 Access token붙여서 GET,POST 호출하면 된다.
탐색페이지에서 폴더를 클릭했을때 폴더의 내용을 가져오는데, 특정폴더의 내용은 다음과 같이 가져온다
HttpClient^ httpClient = ref new HttpClient();
auto headerAuth = ref new Windows::Web::Http::Headers::HttpCredentialsHeaderValue(ref new String(BEARER), m_AccessToken);
httpClient->DefaultRequestHeaders->Authorization = headerAuth;
String^ cmd = nullptr;
if (folder)
{
cmd = ref new String(ROOT_URL) + L"/" + folder->ID + L"/files";
}
else
{
cmd = ref new String(ROOT_URL) + L"/me/skydrive/files";
}
auto uri = ref new Uri(cmd);
auto task_folder = create_task(httpClient->GetStringAsync(uri));
task_folder.then([this,folder](task<String^> task_json)
{
String^ json = nullptr;
try
{
json = task_json.get();
}
catch (Exception^ e)
{
const WCHAR* err = e->Message->Data();
OutputDebugString(err);
}
if (!json)
{
return;
}
Vector<FileItem^>^ items = CreateFileItemsInfoFromJson(json,folder);
m_CurFolder = folder;
m_CurItems = items;
});
그리고 JSON파싱을 해야되는데 아마 이것 때문에 Casablanca를 쓰는것 같다. 그런데 UWP에는 JSON파싱 API가 이미 있다. 굳이 Casablanca를 사용할 필요가 없다.
위 코드에서 폴더안의 파일과 서브폴더들 목록을 받아와서 CreateFileItemsInfoFromJson()이란 함수를 호출하는데, 물론 따로 만든 함수이고 아래처럼 구현했다.
Vector<FileItem^>^ OneDriveService::CreateFileItemsInfoFromJson(String^ jsonStr,FileItem^ ParentFolder)
{
Vector<FileItem^>^ files = ref new Vector<FileItem^>();
Windows::Data::Json::JsonObject^ tokenResponse = ref new JsonObject();
if (!JsonObject::TryParse(jsonStr, &tokenResponse))
return nullptr;
auto map = tokenResponse->GetView();
IJsonValue^ value = map->Lookup("data");
String^ s = value->Stringify();
JsonArray^ mapValue = ref new JsonArray();
if (JsonArray::TryParse(s, &mapValue))
{
auto vec = mapValue->GetView();
for each(auto item in vec)
{
auto vtype = item->ValueType;
switch (vtype)
{
case JsonValueType::Object:
{
JsonObject^ obj = item->GetObject();
FileItem^ fileItem = CreateFileItem(obj);
fileItem->SetParentFolder(ParentFolder);
files->Append(fileItem);
}
break;
default:
__debugbreak();
int a = 0;
}
}
}
return files;
}
FileItem^ OneDriveService::CreateFileItem(Windows::Data::Json::JsonObject^ obj)
{
auto view = obj->GetView();
FileItem^ fileItem = nullptr;
String^ Name = nullptr;
String^ Title = nullptr;
String^ ID = nullptr;
String^ ParentID = nullptr;
SKY_FILE_TYPE type = SKY_FILE_TYPE_ETC;
for each (auto item in view)
{
String^ key = item->Key;
if (key == L"name")
{
Name = item->Value->GetString();
}
if (key == L"id")
{
ID = item->Value->GetString();
}
if (key == L"type")
{
String^ value = item->Value->GetString();
if (L"folder" == value || L"album" == value)
{
type = SKY_FILE_TYPE_FOLDER;
}
else if (L"photo" == value)
{
type = SKY_FILE_TYPE_PHOTO;
}
else if (L"audio" == value)
{
type = SKY_FILE_TYPE_AUDIO;
}
else
{
type = SKY_FILE_TYPE_ETC;
}
}
if (key == L"title")
{
auto value_type = item->Value->ValueType;
if (value_type == JsonValueType::String)
{
Title = item->Value->GetString();
}
}
if (key == L"parent_id")
{
ParentID = item->Value->GetString();
}
}
if (Name && ID)
{
fileItem = ref new FileItem;
fileItem->FileName = Name;
fileItem->ID = ID;
fileItem->ParentID = ParentID;
fileItem->Title = Title;
fileItem->SetType(type);
}
return fileItem;
}
UWP에서 JSON파싱은 별도 라이브러리 없이 간단히 할 수 있다. 게다가 Visual Studio 디버거는 JSon타입의 String을 JSON포맷으로 깔끔하게 보여준다.
현재 폴더 탐색기능, 파일 다운로드 기능까진 구현해놨다.
아 그리고 진짜 C++하는 사람들이 다 말라죽은건지, 그 사람들 중에 OneDrive쓸려는 사람은 한명도 없는건지…
Casablanca 안쓰고 OneDrive억세스하는 C++ 샘플을 한개도 못찾았는데 내가 검색을 못해서 그런거라고 누가 말해줬으면 좋겠네.
아니 Casablanca쓰고 OneDrive억세스하는 샘플도 사실 없다. 해당 문서도 사라졌다. 몇 년전엔 MS에서 작성해서 올려준 live_connect.h라는 파일 한개짜리 클래스가 있었다. 하지만 얼마 후 삭제되었고 더 이상 올라오지 않는다.
조금 더 다듬어서 소스코드 공개할 예정이다. 내가 쪽팔려서 소스코드 공개는 잘 안하는데 이건 너무 없어서 안할 수가 없다.
나처럼 UWP로 DirectX게임과 툴을 개발하고 그 안에서 OneDrive를 쓰고 싶은 사람이 있다면 도움이 되겠지.

