그저 내가 개발중인 게임과 툴에서 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를 쓰고 싶은 사람이 있다면 도움이 되겠지.