tdesktop/Telegram/SourceFiles/storage/file_upload.cpp

747 lines
21 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "storage/file_upload.h"
#include "api/api_editing.h"
#include "api/api_send_progress.h"
#include "storage/localimageloader.h"
#include "storage/file_download.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "ui/image/image_location_factory.h"
#include "history/history_item.h"
#include "core/mime_type.h"
#include "main/main_session.h"
#include "apiwrap.h"
namespace Storage {
namespace {
// max 512kb uploaded at the same time in each session
constexpr auto kMaxUploadFileParallelSize = MTP::kUploadSessionsCount * 512 * 1024;
constexpr auto kDocumentMaxPartsCount = 3000;
// 32kb for tiny document ( < 1mb )
constexpr auto kDocumentUploadPartSize0 = 32 * 1024;
// 64kb for little document ( <= 32mb )
constexpr auto kDocumentUploadPartSize1 = 64 * 1024;
// 128kb for small document ( <= 375mb )
constexpr auto kDocumentUploadPartSize2 = 128 * 1024;
// 256kb for medium document ( <= 750mb )
constexpr auto kDocumentUploadPartSize3 = 256 * 1024;
// 512kb for large document ( <= 1500mb )
constexpr auto kDocumentUploadPartSize4 = 512 * 1024;
// One part each half second, if not uploaded faster.
constexpr auto kUploadRequestInterval = crl::time(500);
// How much time without upload causes additional session kill.
constexpr auto kKillSessionTimeout = 15 * crl::time(000);
[[nodiscard]] const char *ThumbnailFormat(const QString &mime) {
return Core::IsMimeSticker(mime) ? "WEBP" : "JPG";
}
} // namespace
struct Uploader::File {
File(const SendMediaReady &media);
File(const std::shared_ptr<FileLoadResult> &file);
void setDocSize(int32 size);
bool setPartSize(uint32 partSize);
std::shared_ptr<FileLoadResult> file;
SendMediaReady media;
int32 partsCount = 0;
mutable int32 fileSentSize = 0;
uint64 id() const;
SendMediaType type() const;
uint64 thumbId() const;
const QString &filename() const;
HashMd5 md5Hash;
std::unique_ptr<QFile> docFile;
int32 docSentParts = 0;
int32 docSize = 0;
int32 docPartSize = 0;
int32 docPartsCount = 0;
};
Uploader::File::File(const SendMediaReady &media) : media(media) {
partsCount = media.parts.size();
if (type() == SendMediaType::File
|| type() == SendMediaType::ThemeFile
|| type() == SendMediaType::Audio) {
setDocSize(media.file.isEmpty()
? media.data.size()
: media.filesize);
} else {
docSize = docPartSize = docPartsCount = 0;
}
}
Uploader::File::File(const std::shared_ptr<FileLoadResult> &file)
: file(file) {
partsCount = (type() == SendMediaType::Photo
|| type() == SendMediaType::Secure)
? file->fileparts.size()
: file->thumbparts.size();
if (type() == SendMediaType::File
|| type() == SendMediaType::ThemeFile
|| type() == SendMediaType::Audio) {
setDocSize(file->filesize);
} else {
docSize = docPartSize = docPartsCount = 0;
}
}
void Uploader::File::setDocSize(int32 size) {
docSize = size;
constexpr auto limit0 = 1024 * 1024;
constexpr auto limit1 = 32 * limit0;
if (docSize >= limit0 || !setPartSize(kDocumentUploadPartSize0)) {
if (docSize > limit1 || !setPartSize(kDocumentUploadPartSize1)) {
if (!setPartSize(kDocumentUploadPartSize2)) {
if (!setPartSize(kDocumentUploadPartSize3)) {
if (!setPartSize(kDocumentUploadPartSize4)) {
LOG(("Upload Error: bad doc size: %1").arg(docSize));
}
}
}
}
}
}
bool Uploader::File::setPartSize(uint32 partSize) {
docPartSize = partSize;
docPartsCount = (docSize / docPartSize)
+ ((docSize % docPartSize) ? 1 : 0);
return (docPartsCount <= kDocumentMaxPartsCount);
}
uint64 Uploader::File::id() const {
return file ? file->id : media.id;
}
SendMediaType Uploader::File::type() const {
return file ? file->type : media.type;
}
uint64 Uploader::File::thumbId() const {
return file ? file->thumbId : media.thumbId;
}
const QString &Uploader::File::filename() const {
return file ? file->filename : media.filename;
}
Uploader::Uploader(not_null<ApiWrap*> api)
: _api(api) {
nextTimer.setSingleShot(true);
connect(&nextTimer, SIGNAL(timeout()), this, SLOT(sendNext()));
stopSessionsTimer.setSingleShot(true);
connect(&stopSessionsTimer, SIGNAL(timeout()), this, SLOT(stopSessions()));
const auto session = &_api->session();
photoReady(
) | rpl::start_with_next([=](const UploadedPhoto &data) {
if (data.edit) {
const auto item = session->data().message(data.fullId);
Api::EditMessageWithUploadedPhoto(item, data.file, data.options);
} else {
_api->sendUploadedPhoto(
data.fullId,
data.file,
data.options);
}
}, _lifetime);
documentReady(
) | rpl::start_with_next([=](const UploadedDocument &data) {
if (data.edit) {
const auto item = session->data().message(data.fullId);
Api::EditMessageWithUploadedDocument(
item,
data.file,
std::nullopt,
data.options);
} else {
_api->sendUploadedDocument(
data.fullId,
data.file,
std::nullopt,
data.options);
}
}, _lifetime);
thumbDocumentReady(
) | rpl::start_with_next([=](const UploadedThumbDocument &data) {
if (data.edit) {
const auto item = session->data().message(data.fullId);
Api::EditMessageWithUploadedDocument(
item,
data.file,
data.thumb,
data.options);
} else {
_api->sendUploadedDocument(
data.fullId,
data.file,
data.thumb,
data.options);
}
}, _lifetime);
photoProgress(
) | rpl::start_with_next([=](const FullMsgId &fullId) {
processPhotoProgress(fullId);
}, _lifetime);
photoFailed(
) | rpl::start_with_next([=](const FullMsgId &fullId) {
processPhotoFailed(fullId);
}, _lifetime);
documentProgress(
) | rpl::start_with_next([=](const FullMsgId &fullId) {
processDocumentProgress(fullId);
}, _lifetime);
documentFailed(
) | rpl::start_with_next([=](const FullMsgId &fullId) {
processDocumentFailed(fullId);
}, _lifetime);
}
void Uploader::processPhotoProgress(const FullMsgId &newId) {
const auto session = &_api->session();
if (const auto item = session->data().message(newId)) {
const auto photo = item->media()
? item->media()->photo()
: nullptr;
session->sendProgressManager().update(
item->history(),
Api::SendProgressType::UploadPhoto,
0);
session->data().requestItemRepaint(item);
}
}
void Uploader::processDocumentProgress(const FullMsgId &newId) {
const auto session = &_api->session();
if (const auto item = session->data().message(newId)) {
const auto media = item->media();
const auto document = media ? media->document() : nullptr;
const auto sendAction = (document && document->isVoiceMessage())
? Api::SendProgressType::UploadVoice
: Api::SendProgressType::UploadFile;
const auto progress = (document && document->uploading())
? document->uploadingData->offset
: 0;
session->sendProgressManager().update(
item->history(),
sendAction,
progress);
session->data().requestItemRepaint(item);
}
}
void Uploader::processPhotoFailed(const FullMsgId &newId) {
const auto session = &_api->session();
if (const auto item = session->data().message(newId)) {
session->sendProgressManager().update(
item->history(),
Api::SendProgressType::UploadPhoto,
-1);
session->data().requestItemRepaint(item);
}
}
void Uploader::processDocumentFailed(const FullMsgId &newId) {
const auto session = &_api->session();
if (const auto item = session->data().message(newId)) {
const auto media = item->media();
const auto document = media ? media->document() : nullptr;
const auto sendAction = (document && document->isVoiceMessage())
? Api::SendProgressType::UploadVoice
: Api::SendProgressType::UploadFile;
session->sendProgressManager().update(
item->history(),
sendAction,
-1);
session->data().requestItemRepaint(item);
}
}
Uploader::~Uploader() {
clear();
}
Main::Session &Uploader::session() const {
return _api->session();
}
void Uploader::uploadMedia(
const FullMsgId &msgId,
const SendMediaReady &media) {
if (media.type == SendMediaType::Photo) {
session().data().processPhoto(media.photo, media.photoThumbs);
} else if (media.type == SendMediaType::File
|| media.type == SendMediaType::ThemeFile
|| media.type == SendMediaType::Audio) {
const auto document = media.photoThumbs.empty()
? session().data().processDocument(media.document)
: session().data().processDocument(
media.document,
Images::FromImageInMemory(
media.photoThumbs.front().second.image,
"JPG",
media.photoThumbs.front().second.bytes));
if (!media.data.isEmpty()) {
document->setDataAndCache(media.data);
if (media.type == SendMediaType::ThemeFile) {
document->checkWallPaperProperties();
}
}
if (!media.file.isEmpty()) {
document->setLocation(FileLocation(media.file));
}
}
queue.emplace(msgId, File(media));
sendNext();
}
void Uploader::upload(
const FullMsgId &msgId,
const std::shared_ptr<FileLoadResult> &file) {
if (file->type == SendMediaType::Photo) {
const auto photo = session().data().processPhoto(
file->photo,
file->photoThumbs);
photo->uploadingData = std::make_unique<Data::UploadState>(
file->partssize);
} else if (file->type == SendMediaType::File
|| file->type == SendMediaType::ThemeFile
|| file->type == SendMediaType::Audio) {
const auto document = file->thumb.isNull()
? session().data().processDocument(file->document)
: session().data().processDocument(
file->document,
Images::FromImageInMemory(
file->thumb,
ThumbnailFormat(file->filemime),
file->thumbbytes));
document->uploadingData = std::make_unique<Data::UploadState>(
document->size);
if (const auto active = document->activeMediaView()) {
if (!file->goodThumbnail.isNull()) {
active->setGoodThumbnail(std::move(file->goodThumbnail));
}
if (!file->thumb.isNull()) {
active->setThumbnail(file->thumb);
}
}
if (!file->goodThumbnailBytes.isEmpty()) {
document->owner().cache().putIfEmpty(
document->goodThumbnailCacheKey(),
Storage::Cache::Database::TaggedValue(
std::move(file->goodThumbnailBytes),
Data::kImageCacheTag));
}
if (!file->content.isEmpty()) {
document->setDataAndCache(file->content);
}
if (!file->filepath.isEmpty()) {
document->setLocation(FileLocation(file->filepath));
}
if (file->type == SendMediaType::ThemeFile) {
document->checkWallPaperProperties();
}
}
queue.emplace(msgId, File(file));
sendNext();
}
void Uploader::currentFailed() {
auto j = queue.find(uploadingId);
if (j != queue.end()) {
if (j->second.type() == SendMediaType::Photo) {
_photoFailed.fire_copy(j->first);
} else if (j->second.type() == SendMediaType::File
|| j->second.type() == SendMediaType::ThemeFile
|| j->second.type() == SendMediaType::Audio) {
const auto document = session().data().document(j->second.id());
if (document->uploading()) {
document->status = FileUploadFailed;
}
_documentFailed.fire_copy(j->first);
} else if (j->second.type() == SendMediaType::Secure) {
_secureFailed.fire_copy(j->first);
} else {
Unexpected("Type in Uploader::currentFailed.");
}
queue.erase(j);
}
requestsSent.clear();
docRequestsSent.clear();
dcMap.clear();
uploadingId = FullMsgId();
sentSize = 0;
for (int i = 0; i < MTP::kUploadSessionsCount; ++i) {
sentSizes[i] = 0;
}
sendNext();
}
void Uploader::stopSessions() {
for (int i = 0; i < MTP::kUploadSessionsCount; ++i) {
_api->instance().stopSession(MTP::uploadDcId(i));
}
}
void Uploader::sendNext() {
if (sentSize >= kMaxUploadFileParallelSize || _pausedId.msg) {
return;
}
bool stopping = stopSessionsTimer.isActive();
if (queue.empty()) {
if (!stopping) {
stopSessionsTimer.start(kKillSessionTimeout);
}
return;
}
if (stopping) {
stopSessionsTimer.stop();
}
auto i = uploadingId.msg ? queue.find(uploadingId) : queue.begin();
if (!uploadingId.msg) {
uploadingId = i->first;
} else if (i == queue.end()) {
i = queue.begin();
uploadingId = i->first;
}
auto &uploadingData = i->second;
auto todc = 0;
for (auto dc = 1; dc != MTP::kUploadSessionsCount; ++dc) {
if (sentSizes[dc] < sentSizes[todc]) {
todc = dc;
}
}
auto &parts = uploadingData.file
? ((uploadingData.type() == SendMediaType::Photo
|| uploadingData.type() == SendMediaType::Secure)
? uploadingData.file->fileparts
: uploadingData.file->thumbparts)
: uploadingData.media.parts;
const auto partsOfId = uploadingData.file
? ((uploadingData.type() == SendMediaType::Photo
|| uploadingData.type() == SendMediaType::Secure)
? uploadingData.file->id
: uploadingData.file->thumbId)
: uploadingData.media.thumbId;
if (parts.isEmpty()) {
if (uploadingData.docSentParts >= uploadingData.docPartsCount) {
if (requestsSent.empty() && docRequestsSent.empty()) {
const auto options = uploadingData.file
? uploadingData.file->to.options
: Api::SendOptions();
const auto edit = uploadingData.file &&
uploadingData.file->edit;
if (uploadingData.type() == SendMediaType::Photo) {
auto photoFilename = uploadingData.filename();
if (!photoFilename.endsWith(qstr(".jpg"), Qt::CaseInsensitive)) {
// Server has some extensions checking for inputMediaUploadedPhoto,
// so force the extension to be .jpg anyway. It doesn't matter,
// because the filename from inputFile is not used anywhere.
photoFilename += qstr(".jpg");
}
const auto md5 = uploadingData.file
? uploadingData.file->filemd5
: uploadingData.media.jpeg_md5;
const auto file = MTP_inputFile(
MTP_long(uploadingData.id()),
MTP_int(uploadingData.partsCount),
MTP_string(photoFilename),
MTP_bytes(md5));
_photoReady.fire({ uploadingId, options, file, edit });
} else if (uploadingData.type() == SendMediaType::File
|| uploadingData.type() == SendMediaType::ThemeFile
|| uploadingData.type() == SendMediaType::Audio) {
QByteArray docMd5(32, Qt::Uninitialized);
hashMd5Hex(uploadingData.md5Hash.result(), docMd5.data());
const auto file = (uploadingData.docSize > kUseBigFilesFrom)
? MTP_inputFileBig(
MTP_long(uploadingData.id()),
MTP_int(uploadingData.docPartsCount),
MTP_string(uploadingData.filename()))
: MTP_inputFile(
MTP_long(uploadingData.id()),
MTP_int(uploadingData.docPartsCount),
MTP_string(uploadingData.filename()),
MTP_bytes(docMd5));
if (uploadingData.partsCount) {
const auto thumbFilename = uploadingData.file
? uploadingData.file->thumbname
: (qsl("thumb.") + uploadingData.media.thumbExt);
const auto thumbMd5 = uploadingData.file
? uploadingData.file->thumbmd5
: uploadingData.media.jpeg_md5;
const auto thumb = MTP_inputFile(
MTP_long(uploadingData.thumbId()),
MTP_int(uploadingData.partsCount),
MTP_string(thumbFilename),
MTP_bytes(thumbMd5));
_thumbDocumentReady.fire({
uploadingId,
options,
file,
thumb,
edit });
} else {
_documentReady.fire({
uploadingId,
options,
file,
edit });
}
} else if (uploadingData.type() == SendMediaType::Secure) {
_secureReady.fire({
uploadingId,
uploadingData.id(),
uploadingData.partsCount });
}
queue.erase(uploadingId);
uploadingId = FullMsgId();
sendNext();
}
return;
}
auto &content = uploadingData.file
? uploadingData.file->content
: uploadingData.media.data;
QByteArray toSend;
if (content.isEmpty()) {
if (!uploadingData.docFile) {
const auto filepath = uploadingData.file
? uploadingData.file->filepath
: uploadingData.media.file;
uploadingData.docFile = std::make_unique<QFile>(filepath);
if (!uploadingData.docFile->open(QIODevice::ReadOnly)) {
currentFailed();
return;
}
}
toSend = uploadingData.docFile->read(uploadingData.docPartSize);
if (uploadingData.docSize <= kUseBigFilesFrom) {
uploadingData.md5Hash.feed(toSend.constData(), toSend.size());
}
} else {
const auto offset = uploadingData.docSentParts
* uploadingData.docPartSize;
toSend = content.mid(offset, uploadingData.docPartSize);
if ((uploadingData.type() == SendMediaType::File
|| uploadingData.type() == SendMediaType::ThemeFile
|| uploadingData.type() == SendMediaType::Audio)
&& uploadingData.docSentParts <= kUseBigFilesFrom) {
uploadingData.md5Hash.feed(toSend.constData(), toSend.size());
}
}
if ((toSend.size() > uploadingData.docPartSize)
|| ((toSend.size() < uploadingData.docPartSize
&& uploadingData.docSentParts + 1 != uploadingData.docPartsCount))) {
currentFailed();
return;
}
mtpRequestId requestId;
if (uploadingData.docSize > kUseBigFilesFrom) {
requestId = _api->request(MTPupload_SaveBigFilePart(
MTP_long(uploadingData.id()),
MTP_int(uploadingData.docSentParts),
MTP_int(uploadingData.docPartsCount),
MTP_bytes(toSend)
)).done([=](const MTPBool &result, mtpRequestId requestId) {
partLoaded(result, requestId);
}).fail([=](const RPCError &error, mtpRequestId requestId) {
partFailed(error, requestId);
}).toDC(MTP::uploadDcId(todc)).send();
} else {
requestId = _api->request(MTPupload_SaveFilePart(
MTP_long(uploadingData.id()),
MTP_int(uploadingData.docSentParts),
MTP_bytes(toSend)
)).done([=](const MTPBool &result, mtpRequestId requestId) {
partLoaded(result, requestId);
}).fail([=](const RPCError &error, mtpRequestId requestId) {
partFailed(error, requestId);
}).toDC(MTP::uploadDcId(todc)).send();
}
docRequestsSent.emplace(requestId, uploadingData.docSentParts);
dcMap.emplace(requestId, todc);
sentSize += uploadingData.docPartSize;
sentSizes[todc] += uploadingData.docPartSize;
uploadingData.docSentParts++;
} else {
auto part = parts.begin();
const auto requestId = _api->request(MTPupload_SaveFilePart(
MTP_long(partsOfId),
MTP_int(part.key()),
MTP_bytes(part.value())
)).done([=](const MTPBool &result, mtpRequestId requestId) {
partLoaded(result, requestId);
}).fail([=](const RPCError &error, mtpRequestId requestId) {
partFailed(error, requestId);
}).toDC(MTP::uploadDcId(todc)).send();
requestsSent.emplace(requestId, part.value());
dcMap.emplace(requestId, todc);
sentSize += part.value().size();
sentSizes[todc] += part.value().size();
parts.erase(part);
}
nextTimer.start(kUploadRequestInterval);
}
void Uploader::cancel(const FullMsgId &msgId) {
uploaded.erase(msgId);
if (uploadingId == msgId) {
currentFailed();
} else {
queue.erase(msgId);
}
}
void Uploader::pause(const FullMsgId &msgId) {
_pausedId = msgId;
}
void Uploader::unpause() {
_pausedId = FullMsgId();
sendNext();
}
void Uploader::confirm(const FullMsgId &msgId) {
}
void Uploader::clear() {
uploaded.clear();
queue.clear();
for (const auto &requestData : requestsSent) {
_api->request(requestData.first).cancel();
}
requestsSent.clear();
for (const auto &requestData : docRequestsSent) {
_api->request(requestData.first).cancel();
}
docRequestsSent.clear();
dcMap.clear();
sentSize = 0;
for (int i = 0; i < MTP::kUploadSessionsCount; ++i) {
_api->instance().stopSession(MTP::uploadDcId(i));
sentSizes[i] = 0;
}
stopSessionsTimer.stop();
}
void Uploader::partLoaded(const MTPBool &result, mtpRequestId requestId) {
auto j = docRequestsSent.end();
auto i = requestsSent.find(requestId);
if (i == requestsSent.cend()) {
j = docRequestsSent.find(requestId);
}
if (i != requestsSent.cend() || j != docRequestsSent.cend()) {
if (mtpIsFalse(result)) { // failed to upload current file
currentFailed();
return;
} else {
auto dcIt = dcMap.find(requestId);
if (dcIt == dcMap.cend()) { // must not happen
currentFailed();
return;
}
auto dc = dcIt->second;
dcMap.erase(dcIt);
int32 sentPartSize = 0;
auto k = queue.find(uploadingId);
Assert(k != queue.cend());
auto &[fullId, file] = *k;
if (i != requestsSent.cend()) {
sentPartSize = i->second.size();
requestsSent.erase(i);
} else {
sentPartSize = file.docPartSize;
docRequestsSent.erase(j);
}
sentSize -= sentPartSize;
sentSizes[dc] -= sentPartSize;
if (file.type() == SendMediaType::Photo) {
file.fileSentSize += sentPartSize;
const auto photo = session().data().photo(file.id());
if (photo->uploading() && file.file) {
photo->uploadingData->size = file.file->partssize;
photo->uploadingData->offset = file.fileSentSize;
}
_photoProgress.fire_copy(fullId);
} else if (file.type() == SendMediaType::File
|| file.type() == SendMediaType::ThemeFile
|| file.type() == SendMediaType::Audio) {
const auto document = session().data().document(file.id());
if (document->uploading()) {
const auto doneParts = file.docSentParts
- int(docRequestsSent.size());
document->uploadingData->offset = std::min(
document->uploadingData->size,
doneParts * file.docPartSize);
}
_documentProgress.fire_copy(fullId);
} else if (file.type() == SendMediaType::Secure) {
file.fileSentSize += sentPartSize;
_secureProgress.fire_copy({
fullId,
file.fileSentSize,
file.file->partssize });
}
}
}
sendNext();
}
void Uploader::partFailed(const RPCError &error, mtpRequestId requestId) {
// failed to upload current file
if ((requestsSent.find(requestId) != requestsSent.cend())
|| (docRequestsSent.find(requestId) != docRequestsSent.cend())) {
currentFailed();
}
sendNext();
}
} // namespace Storage