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 <mutex>
15
16#include <wx/filefn.h>
17#include <wx/filename.h>
18
19#include <rapidjson/document.h>
20#include <rapidjson/writer.h>
21
22#include "AudacityException.h"
23
24#include "OAuthService.h"
25#include "ServiceConfig.h"
26
27#include "NetworkManager.h"
28#include "Request.h"
29#include "IResponse.h"
30#include "MultipartData.h"
31
32#include "CodeConversions.h"
33
34#include "TempDirectory.h"
35#include "FileNames.h"
36
37namespace cloud::audiocom
38{
39namespace
40{
41std::string_view DeduceMimeType(const wxString& ext)
42{
43 if (ext == "wv")
44 return "audio/x-wavpack";
45 else if (ext == "flac")
46 return "audio/x-flac";
47 else if (ext == "mp3")
48 return "audio/mpeg";
49 else
50 return "audio/x-wav";
51}
52
54 const wxString& filePath, const wxString& projectName, bool isPublic)
55{
56 rapidjson::Document document;
57 document.SetObject();
58
59 const wxFileName fileName(filePath);
60 const auto mimeType = DeduceMimeType(fileName.GetExt());
61
62 document.AddMember(
63 "mime",
64 rapidjson::Value(
65 mimeType.data(), mimeType.length(), document.GetAllocator()),
66 document.GetAllocator());
67
68 const auto downloadMime = GetServiceConfig().GetDownloadMime();
69
70 if (!downloadMime.empty())
71 {
72 document.AddMember(
73 "download_mime",
74 rapidjson::Value(
75 downloadMime.data(), downloadMime.length(),
76 document.GetAllocator()),
77 document.GetAllocator());
78 }
79
80 const auto name = audacity::ToUTF8(projectName.empty() ? fileName.GetFullName() : projectName);
81
82 document.AddMember(
83 "name",
84 rapidjson::Value(name.data(), name.length(), document.GetAllocator()),
85 document.GetAllocator());
86
87 document.AddMember(
88 "size",
89 rapidjson::Value(static_cast<int64_t>(fileName.GetSize().GetValue())),
90 document.GetAllocator());
91
92 document.AddMember(
93 "public", rapidjson::Value(isPublic), document.GetAllocator());
94
95 rapidjson::StringBuffer buffer;
96 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
97 document.Accept(writer);
98
99 return std::string(buffer.GetString());
100}
101
102std::string GetProgressPayload(uint64_t current, uint64_t total)
103{
104 rapidjson::Document document;
105 document.SetObject();
106
107 document.AddMember(
108 "progress", rapidjson::Value(current / static_cast<double>(total) * 100.0),
109 document.GetAllocator());
110
111 rapidjson::StringBuffer buffer;
112 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
113 document.Accept(writer);
114
115 return std::string(buffer.GetString());
116}
117
118
119// This class will capture itself inside the request handlers
120// by a strong reference. This way we ensure that it outlives all
121// the outstanding requests.
124 std::enable_shared_from_this<UploadOperation>
125{
127 const ServiceConfig& serviceConfig, wxString fileName,
128 wxString projectName, bool isPublic,
129 UploadService::CompletedCallback completedCallback,
130 UploadService::ProgressCallback progressCallback)
131 : mServiceConfig(serviceConfig)
132 , mFileName(std::move(fileName))
133 , mProjectName(std::move(projectName))
134 , mIsPublic(isPublic)
135 , mCompletedCallback(std::move(completedCallback))
136 , mProgressCallback(std::move(progressCallback))
137 {
138 }
139
141
142 const wxString mFileName;
143 const wxString mProjectName;
144
145 const bool mIsPublic;
146
149
150 std::string mAuthToken;
151
152 std::string mSuccessUrl;
153 std::string mFailureUrl;
154 std::string mProgressUrl;
155
156 std::string mAudioID;
157 std::string mUploadToken;
158
159 std::string mAudioSlug;
160
161 using Clock = std::chrono::steady_clock;
162
163 Clock::time_point mLastProgressReportTime;
164
165 mutable std::mutex mStatusMutex;
166 mutable std::mutex mCallbacksMutex;
167
168 std::weak_ptr<audacity::network_manager::IResponse> mActiveResponse;
169 bool mCompleted {};
170 bool mAborted {};
171
173 {
174 if (!mAuthToken.empty())
175 request.setHeader(
177 }
178
179 void FailPromise(UploadOperationCompleted::Result result, std::string errorMessage)
180 {
181 {
182 std::lock_guard<std::mutex> lock(mStatusMutex);
183 mCompleted = true;
184 }
185
186 std::lock_guard<std::mutex> callbacksLock(mCallbacksMutex);
187
188 if (mCompletedCallback)
189 {
190 mCompletedCallback(
191 UploadOperationCompleted { result, std::move(errorMessage) });
192 }
193
194 mProgressCallback = {};
195 mCompletedCallback = {};
196 }
197
199 {
200 {
201 std::lock_guard<std::mutex> lock(mStatusMutex);
202 mCompleted = true;
203 }
204
205 std::lock_guard<std::mutex> callbacksLock(mCallbacksMutex);
206
207 if (mCompletedCallback)
208 {
209
210 mCompletedCallback(
212 {},
213 mServiceConfig.GetFinishUploadPage(mAudioID, mUploadToken),
214 mAudioSlug });
215 }
216
217 mProgressCallback = {};
218 mCompletedCallback = {};
219 }
220
221 void InitiateUpload(std::string_view authToken)
222 {
223 using namespace audacity::network_manager;
224
225 Request request(mServiceConfig.GetAPIUrl("/audio"));
226
227 request.setHeader(
229
230 request.setHeader(
232
233 mAuthToken = std::string(authToken);
234 SetAuthHeader(request);
235
236 const auto payload = GetUploadRequestPayload(mFileName, mProjectName, mIsPublic);
237
238 std::lock_guard<std::mutex> lock(mStatusMutex);
239
240 // User has already aborted? Do not send the request.
241 if (mAborted)
242 return;
243
244 auto response = NetworkManager::GetInstance().doPost(
245 request, payload.data(), payload.size());
246
247 mActiveResponse = response;
248
249 response->setRequestFinishedCallback(
250 [response, sharedThis = shared_from_this(), this](auto) {
251 auto responseCode = response->getHTTPCode();
252
253 if (responseCode == 201)
254 {
255 HandleUploadPolicy(response->readAll<std::string>());
256 }
257 else if (responseCode == 401)
258 {
259 FailPromise(
261 response->readAll<std::string>());
262 }
263 else if (responseCode == 422)
264 {
265 FailPromise(
267 response->readAll<std::string>());
268 }
269 else
270 {
271 FailPromise(
273 response->readAll<std::string>());
274 }
275 });
276 }
277
278 void HandleUploadPolicy(std::string uploadPolicyJSON)
279 {
280 using namespace audacity::network_manager;
281
282 rapidjson::Document document;
283 document.Parse(uploadPolicyJSON.data(), uploadPolicyJSON.length());
284
285 if (
286 !document.HasMember("url") || !document.HasMember("success") ||
287 !document.HasMember("fail") || !document.HasMember("progress"))
288 {
289 FailPromise(
291 uploadPolicyJSON);
292
293 return;
294 }
295
296 auto form = std::make_unique<MultipartData>();
297
298 if (document.HasMember("fields"))
299 {
300 const auto& fields = document["fields"];
301
302 for (auto it = fields.MemberBegin(); it != fields.MemberEnd(); ++it)
303 form->Add(it->name.GetString(), it->value.GetString());
304 }
305
306 const auto fileField =
307 document.HasMember("field") ? document["field"].GetString() : "file";
308
309 const wxFileName name { mFileName };
310
311 try
312 {
313 // We have checked for the file existence on the main thread
314 // already. For safety sake check for any exception thrown by AddFile
315 // anyway
316 form->AddFile(fileField, DeduceMimeType(name.GetExt()), name);
317 }
318 catch (...)
319 {
320 // Just fail the promise in case if any exception was thrown
321 // UploadService user is responsible to display an appropriate dialog
323 return;
324 }
325
326
327 const auto url = document["url"].GetString();
328
329 mSuccessUrl = document["success"].GetString();
330 mFailureUrl = document["fail"].GetString();
331 mProgressUrl = document["progress"].GetString();
332
333 if (document.HasMember("extra"))
334 {
335 const auto& extra = document["extra"];
336
337 mAudioID = extra["audio"]["id"].GetString();
338 mAudioSlug = extra["audio"]["slug"].GetString();
339
340 if (extra.HasMember("token"))
341 mUploadToken = extra["token"].GetString();
342 }
343
344 const auto encType = document.HasMember("enctype") ?
345 document["enctype"].GetString() :
346 "multipart/form-data";
347
348 Request request(url);
349
350 request.setHeader(common_headers::ContentType, encType);
351 request.setHeader(
353
354 // We only lock late and for very short time
355 std::lock_guard<std::mutex> lock(mStatusMutex);
356
357 if (mAborted)
358 return;
359
360 auto response =
361 NetworkManager::GetInstance().doPost(request, std::move(form));
362
363 mActiveResponse = response;
364
365 response->setRequestFinishedCallback(
366 [response, sharedThis = shared_from_this(), this](auto)
367 {
368 HandleS3UploadCompleted(response);
369 });
370
371 response->setUploadProgressCallback(
372 [response, sharedThis = shared_from_this(),
373 this](auto current, auto total)
374 { HandleUploadProgress(current, total); });
375 }
376
377 void HandleUploadProgress(uint64_t current, uint64_t total)
378 {
379 {
380 std::lock_guard<std::mutex> callbacksLock(mCallbacksMutex);
381
382 if (mProgressCallback)
383 mProgressCallback(current, total);
384 }
385
386 const auto now = Clock::now();
387
388 if ((now - mLastProgressReportTime) > mServiceConfig.GetProgressCallbackTimeout())
389 {
390 mLastProgressReportTime = now;
391
392 using namespace audacity::network_manager;
393 Request request(mProgressUrl);
394
395 request.setHeader(
397 request.setHeader(
399
400 auto payload = GetProgressPayload(current, total);
401
402 std::lock_guard<std::mutex> lock(mStatusMutex);
403
404 if (mAborted)
405 return;
406
407 auto response = NetworkManager::GetInstance().doPatch(
408 request, payload.data(), payload.size());
409
410 response->setRequestFinishedCallback([response](auto) {});
411 }
412 }
413
414 void HandleS3UploadCompleted(std::shared_ptr<audacity::network_manager::IResponse> response)
415 {
416 using namespace audacity::network_manager;
417
418 const auto responseCode = response->getHTTPCode();
419
420 const bool success =
421 responseCode == 200 || responseCode == 201 || responseCode == 204;
422
423 Request request(success ? mSuccessUrl : mFailureUrl);
424 SetAuthHeader(request);
425
426 std::lock_guard<std::mutex> lock(mStatusMutex);
427
428 if (mAborted)
429 return;
430
431 auto finalResponse = success ? NetworkManager::GetInstance().doPost(request, nullptr, 0) :
432 NetworkManager::GetInstance().doDelete(request);
433
434 mActiveResponse = finalResponse;
435
436 finalResponse->setRequestFinishedCallback(
437 [finalResponse, sharedThis = shared_from_this(), this, success](auto)
438 {
439 const auto httpCode = finalResponse->getHTTPCode();
440 if (success && httpCode >= 200 && httpCode < 300)
441 {
442 CompletePromise();
443 return;
444 }
445
446 FailPromise(
448 finalResponse->readAll<std::string>());
449 });
450 }
451
452 bool IsCompleted() override
453 {
454 std::lock_guard<std::mutex> lock(mStatusMutex);
455 return mCompleted;
456 }
457
458 void Abort() override
459 {
460 {
461 std::lock_guard<std::mutex> lock(mStatusMutex);
462
463 if (mCompleted)
464 return;
465
466 mCompleted = true;
467 mAborted = true;
468
469 if (auto activeResponse = mActiveResponse.lock())
470 activeResponse->abort();
471 }
472
473 std::lock_guard<std::mutex> callbacksLock(mCallbacksMutex);
474
475 if (mCompletedCallback)
476 mCompletedCallback({ UploadOperationCompleted::Result::Aborted });
477
478 mCompletedCallback = {};
479 mProgressCallback = {};
480 }
481
482
483 void DiscardResult() override
484 {
485 using namespace audacity::network_manager;
486
487 Abort();
488
489 auto url = mServiceConfig.GetAPIUrl("/audio");
490 url += "/" + mAudioID + "?token=" + mUploadToken;
491
492 Request request(url);
493 auto response = NetworkManager::GetInstance().doDelete(request);
494
495 response->setRequestFinishedCallback(
496 [response](auto)
497 {
498 // Do nothing
499 });
500 }
501}; // struct UploadOperation
502} // namespace
503
505 : mServiceConfig(config), mOAuthService(service)
506{
507}
508
510 const wxString& fileName, const wxString& projectName, bool isPublic,
511 CompletedCallback completedCallback, ProgressCallback progressCallback)
512{
513 if (!wxFileExists(fileName))
514 {
515 if (completedCallback)
516 completedCallback(UploadOperationCompleted {
518
519 return {};
520 }
521
522 auto operation = std::make_shared<AudiocomUploadOperation>(
523 mServiceConfig, fileName, projectName, isPublic,
524 std::move(completedCallback), std::move(progressCallback));
525
526 mOAuthService.ValidateAuth([operation](std::string_view authToken)
527 { operation->InitiateUpload(authToken); });
528
529 return UploadOperationHandle { operation };
530}
531
533
535 std::shared_ptr<UploadOperation> operation)
536 : mOperation(std::move(operation))
537{
538}
539
541{
542 if (mOperation)
543 // It is safe to call Abort on completed operations
544 mOperation->Abort();
545}
546
547UploadOperationHandle::operator bool() const noexcept
548{
549 return mOperation != nullptr;
550}
551
553{
554 return mOperation.operator->();
555}
556
558{
559 const auto tempPath = TempDirectory::DefaultTempDir();
560
561 if (!wxDirExists(tempPath))
562 {
563 // Temp directory was not created yet.
564 // Is it a first run of Audacity?
565 // In any case, let's wait for some better time
566 return {};
567 }
568
570 tempPath, XO("Cannot proceed to upload.")))
571 return {};
572
573 return tempPath + "/cloud/";
574}
575
576namespace
577{
579 const auto tempPath = GetUploadTempPath();
580
581 if (!wxDirExists(tempPath))
582 return;
583
584 wxArrayString files;
585
586 wxDir::GetAllFiles(tempPath, &files, {}, wxDIR_FILES);
587
588 for (const auto& file : files)
589 wxRemoveFile(file);
590
591 return;
592});
593}
594
595} // namespace 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:74
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
Request & setHeader(const std::string &name, std::string value)
Definition: Request.cpp:46
Service responsible for OAuth authentication against the audio.com service.
Definition: OAuthService.h:38
void ValidateAuth(std::function< void(std::string_view)> completedHandler)
Attempt to authorize the user.
Configuration for the audio.com.
Definition: ServiceConfig.h:24
std::string GetAPIUrl(std::string_view apiURI) const
Helper to construct the full URLs for the API.
std::chrono::milliseconds GetProgressCallbackTimeout() const
Timeout between progress callbacks.
MimeType GetDownloadMime() const
Return the mime type server should store the file. This is a requirement from audiocom.
std::string GetFinishUploadPage(std::string_view audioID, std::string_view token) const
Helper to construct the page URL for the anonymous upload last stage.
A unique_ptr like class that holds a pointer to UploadOperation.
Definition: UploadService.h:75
UploadOperation * operator->() const noexcept
std::shared_ptr< UploadOperation > mOperation
Definition: UploadService.h:92
Class used to track the upload operation.
Definition: UploadService.h:60
std::function< void(uint64_t current, uint64_t total)> ProgressCallback
UploadOperationHandle Upload(const wxString &fileName, const wxString &projectName, bool isPublic, CompletedCallback completedCallback, ProgressCallback progressCallback)
Uploads the file to audio.com.
const ServiceConfig & mServiceConfig
UploadService(const ServiceConfig &config, OAuthService &service)
std::function< void(const UploadOperationCompleted &)> CompletedCallback
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()
FrameStatistics & GetInstance() noexcept
std::string ToUTF8(const std::wstring &wstr)
std::string GetProgressPayload(uint64_t current, uint64_t total)
std::string_view DeduceMimeType(const wxString &ext)
std::string GetUploadRequestPayload(const wxString &filePath, const wxString &projectName, bool isPublic)
wxString GetUploadTempPath()
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
STL namespace.
@ FileNotFound
Specified file is not found.
@ InvalidData
audio.com has failed to understand what Audacity wants
@ Aborted
Upload was aborted by the user.
@ UnexpectedResponse
Audacity has failed to understand audio.com response.
@ UploadFailed
Upload failed for some other reason.
void HandleS3UploadCompleted(std::shared_ptr< audacity::network_manager::IResponse > response)
AudiocomUploadOperation(const ServiceConfig &serviceConfig, wxString fileName, wxString projectName, bool isPublic, UploadService::CompletedCallback completedCallback, UploadService::ProgressCallback progressCallback)
void FailPromise(UploadOperationCompleted::Result result, std::string errorMessage)
bool IsCompleted() override
Returns true if the upload is finished.
std::weak_ptr< audacity::network_manager::IResponse > mActiveResponse
void SetAuthHeader(audacity::network_manager::Request &request) const