Audacity 3.2.0
UploadService.cpp
Go to the documentation of this file.
1/* SPDX-License-Identifier: GPL-2.0-or-later */
2/*!********************************************************************
3
4 Audacity: A Digital Audio Editor
5
6 UploadService.cpp
7
8 Dmitry Vedenko
9
10**********************************************************************/
11
12#include "UploadService.h"
13
14#include <cassert>
15#include <mutex>
16
17#include <wx/filefn.h>
18#include <wx/filename.h>
19
20#include <rapidjson/document.h>
21#include <rapidjson/writer.h>
22
23#include "AudacityException.h"
24
25#include "OAuthService.h"
26#include "ServiceConfig.h"
27
28#include "NetworkManager.h"
29#include "Request.h"
30#include "IResponse.h"
31#include "MultipartData.h"
32
33#include "CodeConversions.h"
34
35#include "TempDirectory.h"
36#include "FileNames.h"
37
39{
40namespace
41{
42std::string_view DeduceMimeType(const wxString& ext)
43{
44 if (ext == "wv")
45 return "audio/x-wavpack";
46 else if (ext == "flac")
47 return "audio/x-flac";
48 else if (ext == "mp3")
49 return "audio/mpeg";
50 else
51 return "audio/x-wav";
52}
53
55 const wxString& filePath, const wxString& projectName, bool isPublic)
56{
57 rapidjson::Document document;
58 document.SetObject();
59
60 const wxFileName fileName(filePath);
61 const auto mimeType = DeduceMimeType(fileName.GetExt());
62
63 document.AddMember(
64 "mime",
65 rapidjson::Value(
66 mimeType.data(), mimeType.length(), document.GetAllocator()),
67 document.GetAllocator());
68
69 const auto downloadMime = GetServiceConfig().GetDownloadMime();
70
71 if (!downloadMime.empty())
72 {
73 document.AddMember(
74 "download_mime",
75 rapidjson::Value(
76 downloadMime.data(), downloadMime.length(),
77 document.GetAllocator()),
78 document.GetAllocator());
79 }
80
81 const auto name = audacity::ToUTF8(projectName.empty() ? fileName.GetFullName() : projectName);
82
83 document.AddMember(
84 "name",
85 rapidjson::Value(name.data(), name.length(), document.GetAllocator()),
86 document.GetAllocator());
87
88 document.AddMember(
89 "size",
90 rapidjson::Value(static_cast<int64_t>(fileName.GetSize().GetValue())),
91 document.GetAllocator());
92
93 document.AddMember(
94 "public", rapidjson::Value(isPublic), document.GetAllocator());
95
96 rapidjson::StringBuffer buffer;
97 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
98 document.Accept(writer);
99
100 return std::string(buffer.GetString());
101}
102
103std::string GetProgressPayload(uint64_t current, uint64_t total)
104{
105 rapidjson::Document document;
106 document.SetObject();
107
108 document.AddMember(
109 "progress", rapidjson::Value(current / static_cast<double>(total) * 100.0),
110 document.GetAllocator());
111
112 rapidjson::StringBuffer buffer;
113 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
114 document.Accept(writer);
115
116 return std::string(buffer.GetString());
117}
118
119UploadFailedPayload ParseUploadFailedMessage(const std::string& payloadText)
120{
121 rapidjson::StringStream stream(payloadText.c_str());
122 rapidjson::Document document;
123
124 document.ParseStream(stream);
125
126 if (!document.IsObject())
127 {
128 // This is unexpected, just return an empty object
129 assert(document.IsObject());
130 return {};
131 }
132
133 UploadFailedPayload payload;
134
135 auto readInt = [&document](const char* name) {
136 return document.HasMember(name) && document[name].IsInt() ?
137 document[name].GetInt() :
138 0;
139 };
140
141 auto readString = [&document](const char* name) -> const char*
142 {
143 return document.HasMember(name) && document[name].IsString() ?
144 document[name].GetString() :
145 "";
146 };
147
148 payload.code = readInt("code");
149 payload.status = readInt("status");
150
151 payload.name = readString("name");
152 payload.message = readString("message");
153
154 if (document.HasMember("errors") && document["errors"].IsObject())
155 {
156 for (auto& err : document["errors"].GetObject ())
157 {
158 if (!err.value.IsString())
159 continue;
160
161 payload.additionalErrors.emplace_back(
162 err.name.GetString(), err.value.GetString());
163 }
164 }
165
166 return payload;
167}
168
169
170// This class will capture itself inside the request handlers
171// by a strong reference. This way we ensure that it outlives all
172// the outstanding requests.
175 std::enable_shared_from_this<UploadOperation>
176{
178 const ServiceConfig& serviceConfig, wxString fileName,
179 wxString projectName, bool isPublic,
180 UploadService::CompletedCallback completedCallback,
181 UploadService::ProgressCallback progressCallback)
182 : mServiceConfig(serviceConfig)
183 , mFileName(std::move(fileName))
184 , mProjectName(std::move(projectName))
185 , mIsPublic(isPublic)
186 , mCompletedCallback(std::move(completedCallback))
187 , mProgressCallback(std::move(progressCallback))
188 {
189 }
190
192
193 const wxString mFileName;
194 const wxString mProjectName;
195
196 const bool mIsPublic;
197
200
201 std::string mAuthToken;
202
203 std::string mSuccessUrl;
204 std::string mFailureUrl;
205 std::string mProgressUrl;
206
207 std::string mAudioID;
208 std::string mUploadToken;
209 std::string mUserName;
210
211 std::string mAudioSlug;
212
213 using Clock = std::chrono::steady_clock;
214
215 Clock::time_point mLastProgressReportTime;
216
217 mutable std::mutex mStatusMutex;
218 mutable std::mutex mCallbacksMutex;
219
220 std::weak_ptr<audacity::network_manager::IResponse> mActiveResponse;
221 bool mCompleted {};
222 bool mAborted {};
223
225 {
226 if (!mAuthToken.empty())
227 request.setHeader(
229
230 const auto language = mServiceConfig.GetAcceptLanguageValue();
231
232 if (!language.empty())
233 request.setHeader(
235 language);
236 }
237
238 void FailPromise(UploadOperationCompleted::Result result, std::string errorMessage)
239 {
240 {
241 std::lock_guard<std::mutex> lock(mStatusMutex);
242 mCompleted = true;
243 }
244
245 std::lock_guard<std::mutex> callbacksLock(mCallbacksMutex);
246
247 if (mCompletedCallback)
248 {
249 mCompletedCallback(
250 UploadOperationCompleted { result, ParseUploadFailedMessage(errorMessage) });
251 }
252
253 mProgressCallback = {};
254 mCompletedCallback = {};
255 }
256
258 {
259 {
260 std::lock_guard<std::mutex> lock(mStatusMutex);
261 mCompleted = true;
262 }
263
264 std::lock_guard<std::mutex> callbacksLock(mCallbacksMutex);
265
266 if (mCompletedCallback)
267 {
268 const auto uploadURL =
269 mAuthToken.empty() ?
270 mServiceConfig.GetFinishUploadPage(mAudioID, mUploadToken) :
271 mServiceConfig.GetAudioURL(mUserName, mAudioSlug);
272
273 mCompletedCallback(
275 UploadSuccessfulPayload { mAudioID, mAudioSlug, mUploadToken, uploadURL } });
276 }
277
278 mProgressCallback = {};
279 mCompletedCallback = {};
280 }
281
282 void InitiateUpload(std::string_view authToken)
283 {
284 using namespace audacity::network_manager;
285
286 Request request(mServiceConfig.GetAPIUrl("/audio"));
287
288 request.setHeader(
290
291 request.setHeader(
293
294 mAuthToken = std::string(authToken);
295 SetRequiredHeaders(request);
296
297 const auto payload = GetUploadRequestPayload(mFileName, mProjectName, mIsPublic);
298
299 std::lock_guard<std::mutex> lock(mStatusMutex);
300
301 // User has already aborted? Do not send the request.
302 if (mAborted)
303 return;
304
305 auto response = NetworkManager::GetInstance().doPost(
306 request, payload.data(), payload.size());
307
308 mActiveResponse = response;
309
310 response->setRequestFinishedCallback(
311 [response, sharedThis = shared_from_this(), this](auto) {
312 auto responseCode = response->getHTTPCode();
313
314 if (responseCode == 201)
315 {
316 HandleUploadPolicy(response->readAll<std::string>());
317 }
318 else if (responseCode == 401)
319 {
320 FailPromise(
322 response->readAll<std::string>());
323 }
324 else if (responseCode == 422)
325 {
326 FailPromise(
328 response->readAll<std::string>());
329 }
330 else
331 {
332 FailPromise(
334 response->readAll<std::string>());
335 }
336 });
337 }
338
339 void HandleUploadPolicy(std::string uploadPolicyJSON)
340 {
341 using namespace audacity::network_manager;
342
343 rapidjson::Document document;
344 document.Parse(uploadPolicyJSON.data(), uploadPolicyJSON.length());
345
346 if (
347 !document.HasMember("url") || !document.HasMember("success") ||
348 !document.HasMember("fail") || !document.HasMember("progress"))
349 {
350 FailPromise(
352 uploadPolicyJSON);
353
354 return;
355 }
356
357 auto form = std::make_unique<MultipartData>();
358
359 if (document.HasMember("fields"))
360 {
361 const auto& fields = document["fields"];
362
363 for (auto it = fields.MemberBegin(); it != fields.MemberEnd(); ++it)
364 form->Add(it->name.GetString(), it->value.GetString());
365 }
366
367 const auto fileField =
368 document.HasMember("field") ? document["field"].GetString() : "file";
369
370 const wxFileName name { mFileName };
371
372 try
373 {
374 // We have checked for the file existence on the main thread
375 // already. For safety sake check for any exception thrown by AddFile
376 // anyway
377 form->AddFile(fileField, DeduceMimeType(name.GetExt()), name);
378 }
379 catch (...)
380 {
381 // Just fail the promise in case if any exception was thrown
382 // UploadService user is responsible to display an appropriate dialog
384 return;
385 }
386
387
388 const auto url = document["url"].GetString();
389
390 mSuccessUrl = document["success"].GetString();
391 mFailureUrl = document["fail"].GetString();
392 mProgressUrl = document["progress"].GetString();
393
394 if (document.HasMember("extra"))
395 {
396 const auto& extra = document["extra"];
397
398 mAudioID = extra["audio"]["id"].GetString();
399 mAudioSlug = extra["audio"]["slug"].GetString();
400
401 if (extra.HasMember("token"))
402 mUploadToken = extra["token"].GetString();
403
404 mUserName = extra["audio"]["username"].GetString();
405 }
406
407 const auto encType = document.HasMember("enctype") ?
408 document["enctype"].GetString() :
409 "multipart/form-data";
410
411 Request request(url);
412
413 request.setHeader(common_headers::ContentType, encType);
414 request.setHeader(
416
417 // We only lock late and for very short time
418 std::lock_guard<std::mutex> lock(mStatusMutex);
419
420 if (mAborted)
421 return;
422
423 auto response =
424 NetworkManager::GetInstance().doPost(request, std::move(form));
425
426 mActiveResponse = response;
427
428 response->setRequestFinishedCallback(
429 [response, sharedThis = shared_from_this(), this](auto)
430 {
431 HandleS3UploadCompleted(response);
432 });
433
434 response->setUploadProgressCallback(
435 [response, sharedThis = shared_from_this(),
436 this](auto current, auto total)
437 { HandleUploadProgress(current, total); });
438 }
439
440 void HandleUploadProgress(uint64_t current, uint64_t total)
441 {
442 {
443 std::lock_guard<std::mutex> callbacksLock(mCallbacksMutex);
444
445 if (mProgressCallback)
446 mProgressCallback(current, total);
447 }
448
449 const auto now = Clock::now();
450
451 if ((now - mLastProgressReportTime) > mServiceConfig.GetProgressCallbackTimeout())
452 {
453 mLastProgressReportTime = now;
454
455 using namespace audacity::network_manager;
456 Request request(mProgressUrl);
457
458 request.setHeader(
460 request.setHeader(
462
463 auto payload = GetProgressPayload(current, total);
464
465 std::lock_guard<std::mutex> lock(mStatusMutex);
466
467 if (mAborted)
468 return;
469
470 auto response = NetworkManager::GetInstance().doPatch(
471 request, payload.data(), payload.size());
472
473 response->setRequestFinishedCallback([response](auto) {});
474 }
475 }
476
477 void HandleS3UploadCompleted(std::shared_ptr<audacity::network_manager::IResponse> response)
478 {
479 using namespace audacity::network_manager;
480
481 const auto responseCode = response->getHTTPCode();
482
483 const bool success =
484 responseCode == 200 || responseCode == 201 || responseCode == 204;
485
486 Request request(success ? mSuccessUrl : mFailureUrl);
487 SetRequiredHeaders(request);
488
489 std::lock_guard<std::mutex> lock(mStatusMutex);
490
491 if (mAborted)
492 return;
493
494 auto finalResponse = success ? NetworkManager::GetInstance().doPost(request, nullptr, 0) :
496
497 mActiveResponse = finalResponse;
498
499 finalResponse->setRequestFinishedCallback(
500 [finalResponse, sharedThis = shared_from_this(), this, success](auto)
501 {
502 const auto httpCode = finalResponse->getHTTPCode();
503 if (success && httpCode >= 200 && httpCode < 300)
504 {
505 CompletePromise();
506 return;
507 }
508
509 FailPromise(
511 finalResponse->readAll<std::string>());
512 });
513 }
514
515 bool IsCompleted() override
516 {
517 std::lock_guard<std::mutex> lock(mStatusMutex);
518 return mCompleted;
519 }
520
521 void Abort() override
522 {
523 {
524 std::lock_guard<std::mutex> lock(mStatusMutex);
525
526 if (mCompleted)
527 return;
528
529 mCompleted = true;
530 mAborted = true;
531
532 if (auto activeResponse = mActiveResponse.lock())
533 activeResponse->abort();
534 }
535
536 std::lock_guard<std::mutex> callbacksLock(mCallbacksMutex);
537
538 if (mCompletedCallback)
539 mCompletedCallback({ UploadOperationCompleted::Result::Aborted });
540
541 mCompletedCallback = {};
542 mProgressCallback = {};
543 }
544
545
546 void DiscardResult() override
547 {
548 using namespace audacity::network_manager;
549
550 Abort();
551
552 auto url = mServiceConfig.GetAPIUrl("/audio");
553 url += "/" + mAudioID + "?token=" + mUploadToken;
554
555 Request request(url);
556 auto response = NetworkManager::GetInstance().doDelete(request);
557
558 response->setRequestFinishedCallback(
559 [response](auto)
560 {
561 // Do nothing
562 });
563 }
564}; // struct UploadOperation
565} // namespace
566
568 : mServiceConfig(config), mOAuthService(service)
569{
570}
571
573 const wxString& fileName, const wxString& projectName, bool isPublic,
574 CompletedCallback completedCallback, ProgressCallback progressCallback)
575{
576 if (!wxFileExists(fileName))
577 {
578 if (completedCallback)
579 completedCallback(UploadOperationCompleted {
581
582 return {};
583 }
584
585 auto operation = std::make_shared<AudiocomUploadOperation>(
586 mServiceConfig, fileName, projectName, isPublic,
587 std::move(completedCallback), std::move(progressCallback));
588
589 mOAuthService.ValidateAuth([operation, this](std::string_view authToken)
590 { operation->InitiateUpload(authToken); }, false);
591
592 return UploadOperationHandle { operation };
593}
594
596
598 std::shared_ptr<UploadOperation> operation)
599 : mOperation(std::move(operation))
600{
601}
602
604{
605 if (mOperation)
606 // It is safe to call Abort on completed operations
607 mOperation->Abort();
608}
609
610UploadOperationHandle::operator bool() const noexcept
611{
612 return mOperation != nullptr;
613}
614
616{
617 return mOperation.operator->();
618}
619
621{
622 const auto tempPath = TempDirectory::DefaultTempDir();
623
624 if (!wxDirExists(tempPath))
625 {
626 // Temp directory was not created yet.
627 // Is it a first run of Audacity?
628 // In any case, let's wait for some better time
629 return {};
630 }
631
633 tempPath, XO("Cannot proceed to upload.")))
634 return {};
635
636 return tempPath + "/cloud/";
637}
638
639namespace
640{
642 const auto tempPath = GetUploadTempPath();
643
644 if (!wxDirExists(tempPath))
645 return;
646
647 wxArrayString files;
648
649 wxDir::GetAllFiles(tempPath, &files, {}, wxDIR_FILES);
650
651 for (const auto& file : files)
652 wxRemoveFile(file);
653
654 return;
655});
656}
657
658} // namespace audacity::cloud::audiocom
Declare abstract class AudacityException, some often-used subclasses, and GuardedCall.
Declare functions to perform UTF-8 to std::wstring conversions.
const TranslatableString name
Definition: Distortion.cpp:76
XO("Cut/Copy/Paste")
Declare an interface for HTTP response.
Declare a class for performing HTTP requests.
Declare a class for constructing HTTP requests.
Subscription Subscribe(Callback callback)
Connect a callback to the Publisher; later-connected are called earlier.
Definition: Observer.h:199
Service responsible for OAuth authentication against the audio.com service.
Definition: OAuthService.h:40
void ValidateAuth(std::function< void(std::string_view)> completedHandler, bool silent)
Attempt to authorize the user.
Configuration for the audio.com.
Definition: ServiceConfig.h:23
std::string GetFinishUploadPage(std::string_view audioID, std::string_view token) const
Helper to construct the page URL for the anonymous upload last stage.
std::chrono::milliseconds GetProgressCallbackTimeout() const
Timeout between progress callbacks.
std::string GetAPIUrl(std::string_view apiURI) const
Helper to construct the full URLs for the API.
std::string GetAudioURL(std::string_view userSlug, std::string_view audioSlug) const
Helper to construct the page URL for the authorised upload.
std::string GetAcceptLanguageValue() const
Returns the preferred language.
A unique_ptr like class that holds a pointer to UploadOperation.
std::shared_ptr< UploadOperation > mOperation
UploadOperation * operator->() const noexcept
Class used to track the upload operation.
Definition: UploadService.h:90
UploadOperationHandle Upload(const wxString &fileName, const wxString &projectName, bool isPublic, CompletedCallback completedCallback, ProgressCallback progressCallback)
Uploads the file to audio.com.
std::function< void(uint64_t current, uint64_t total)> ProgressCallback
UploadService(const ServiceConfig &config, OAuthService &service)
std::function< void(const UploadOperationCompleted &)> CompletedCallback
ResponsePtr doPost(const Request &request, const void *data, size_t size)
ResponsePtr doDelete(const Request &request)
ResponsePtr doPatch(const Request &request, const void *data, size_t size)
Request & setHeader(const std::string &name, std::string value)
Definition: Request.cpp:46
FILES_API bool WritableLocationCheck(const FilePath &path, const TranslatableString &message)
Check location on writable access and return true if checked successfully.
FILES_API const FilePath & DefaultTempDir()
FILES_API Observer::Publisher< FilePath > & GetTempPathObserver()
std::string GetUploadRequestPayload(const wxString &filePath, const wxString &projectName, bool isPublic)
std::string GetProgressPayload(uint64_t current, uint64_t total)
UploadFailedPayload ParseUploadFailedMessage(const std::string &payloadText)
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
std::string ToUTF8(const std::wstring &wstr)
STL namespace.
This structure represents an upload error as returned by the server.
Definition: UploadService.h:28
std::vector< AdditionalError > additionalErrors
Definition: UploadService.h:36
@ InvalidData
audio.com has failed to understand what Audacity wants
@ UnexpectedResponse
Audacity has failed to understand audio.com response.
@ UploadFailed
Upload failed for some other reason.
This structure represents the payload associated with successful upload.
Definition: UploadService.h:41
void HandleS3UploadCompleted(std::shared_ptr< audacity::network_manager::IResponse > response)
void FailPromise(UploadOperationCompleted::Result result, std::string errorMessage)
AudiocomUploadOperation(const ServiceConfig &serviceConfig, wxString fileName, wxString projectName, bool isPublic, UploadService::CompletedCallback completedCallback, UploadService::ProgressCallback progressCallback)