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