|
|
|
|
@ -308,10 +308,10 @@ void AvatarManager::scheduleOwn(quint64 group) {
|
|
|
|
|
void AvatarManager::enqueue(const Job& job) { |
|
|
|
|
for (const auto& old : m_jobs) if ((old.upload == job.upload && old.ref.group == job.ref.group && old.ref.owner == job.ref.owner && |
|
|
|
|
(job.upload || old.ref.hash == job.ref.hash)) || |
|
|
|
|
(!job.upload && !old.upload && old.ref.hash == job.ref.hash)) return; |
|
|
|
|
(!job.upload && !old.upload && old.ref.hash == job.ref.hash && old.original == job.original)) return; |
|
|
|
|
for (const auto& old : m_queue) if ((old.upload == job.upload && old.ref.group == job.ref.group && old.ref.owner == job.ref.owner && |
|
|
|
|
(job.upload || old.ref.hash == job.ref.hash)) || |
|
|
|
|
(!job.upload && !old.upload && old.ref.hash == job.ref.hash)) return; |
|
|
|
|
(!job.upload && !old.upload && old.ref.hash == job.ref.hash && old.original == job.original)) return; |
|
|
|
|
if (job.upload) m_queue.prepend(job); else m_queue.enqueue(job); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -320,9 +320,10 @@ void AvatarManager::pump() {
|
|
|
|
|
while (m_jobs.size() < 2 && !m_queue.isEmpty()) { |
|
|
|
|
Job job = m_queue.dequeue(); const QString context = key(job.ref.group, job.ref.owner); |
|
|
|
|
if (!m_refs.contains(context) || (job.upload && job.generation != m_generation) || |
|
|
|
|
(!job.upload && (m_refs[context].hash != job.ref.hash || m_refs[context].server != job.ref.server))) continue; |
|
|
|
|
(job.gallery && (job.ref.group != m_galleryRef.group || job.ref.owner != m_galleryRef.owner || job.ref.server != m_galleryRef.server)) || |
|
|
|
|
(!job.upload && !job.gallery && (m_refs[context].hash != job.ref.hash || m_refs[context].server != job.ref.server))) continue; |
|
|
|
|
do { job.id = QRandomGenerator::global()->generate64(); } while (!job.id || m_jobs.contains(job.id)); |
|
|
|
|
job.local = m_directory + '/' + job.ref.hash + (job.upload ? QStringLiteral(".png") : QString(".download-%1").arg(job.id)); |
|
|
|
|
job.local = m_directory + '/' + (job.upload ? job.file : QString(".download-%1").arg(job.id)); |
|
|
|
|
m_jobs.insert(job.id, job); auto& stored = m_jobs[job.id]; |
|
|
|
|
if (job.upload) { if (job.ref.server) post(stored, MF_MKDIR); else publish(stored); } |
|
|
|
|
else transfer(stored); |
|
|
|
|
@ -338,7 +339,7 @@ bool AvatarManager::post(Job& job, int command) {
|
|
|
|
|
quint64 id; do { id = QRandomGenerator::global()->generate64(); } while (!id || m_jobs.contains(id)); |
|
|
|
|
Job next = job; m_jobs.remove(job.id); next.id = id; next.stage = command; m_jobs.insert(id, next); |
|
|
|
|
q->message.request_id = id; |
|
|
|
|
const QByteArray path = command == MF_MKDIR ? QByteArray("avatars") : QByteArray("avatars/") + next.ref.hash.toLatin1() + ".png"; |
|
|
|
|
const QByteArray path = command == MF_MKDIR ? QByteArray("avatars") : QByteArray("avatars/") + next.file.toLatin1(); |
|
|
|
|
memcpy(q->message.path, path.constData(), path.size()); |
|
|
|
|
gui_bridge_post_uasync_fn(member_files_gui_request, q); return true; |
|
|
|
|
} |
|
|
|
|
@ -347,8 +348,11 @@ void AvatarManager::transfer(Job& job) {
|
|
|
|
|
auto* q = (mf_transfer_request*)u_calloc(1, sizeof(mf_transfer_request)); |
|
|
|
|
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "avatar: transfer allocation failed"); finish(job.id, MF_IO_ERROR); return; } |
|
|
|
|
q->inst = gui_bridge_get_inst(); q->id = job.id; q->group = job.ref.group; q->server = job.ref.server; |
|
|
|
|
q->owner = job.ref.owner; q->upload = job.upload; q->max_bytes = AvatarCache::MaxBytes; |
|
|
|
|
const QByteArray local = job.local.toUtf8(), remote = "avatars/" + job.ref.hash.toLatin1() + ".png"; |
|
|
|
|
q->owner = job.ref.owner; q->upload = job.upload; |
|
|
|
|
q->max_bytes = job.file.endsWith(".json") ? MF_AVATAR_MANIFEST_MAX_BYTES : |
|
|
|
|
(!job.upload && !job.original ? AvatarCache::MaxBytes : AvatarCache::OriginalMaxBytes); |
|
|
|
|
job.local = m_directory + '/' + (job.upload ? job.file : QString(".download-%1").arg(job.id)); |
|
|
|
|
const QByteArray local = job.local.toUtf8(), remote = "avatars/" + job.file.toLatin1(); |
|
|
|
|
if (local.size() > MF_PATH_MAX) { u_free(q); finish(job.id, MF_INVALID_PATH); return; } |
|
|
|
|
memcpy(q->local, local.constData(), local.size()); memcpy(q->remote, remote.constData(), remote.size()); job.stage = 99; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "avatar: %s group=%llu owner=%016llx server=%016llx hash=%s", |
|
|
|
|
@ -362,20 +366,57 @@ void AvatarManager::publish(Job& job) {
|
|
|
|
|
auto* q = (chat_avatar_req*)u_calloc(1, sizeof(chat_avatar_req)); |
|
|
|
|
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "avatar: publication allocation failed"); finish(job.id, MF_IO_ERROR); return; } |
|
|
|
|
q->inst = gui_bridge_get_inst(); q->id = job.id; q->group = job.ref.group; q->server = job.ref.server; |
|
|
|
|
if (job.ref.server) { const QByteArray file = job.ref.hash.toLatin1() + ".png"; memcpy(q->file, file.constData(), file.size()); } |
|
|
|
|
if (job.ref.server) { const QByteArray file = job.ref.hash.toLatin1() + ".json"; memcpy(q->file, file.constData(), file.size()); } |
|
|
|
|
job.stage = 100; gui_bridge_post_uasync_fn(chat_core_set_my_avatar_trampoline, q); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AvatarManager::onResponse(const uint8_t* data, int length) { |
|
|
|
|
if (length < 16 + MF_WIRE_HEADER) return; |
|
|
|
|
mf_message response{}; if (mf_decode(data + 16, length - 16, &response) < 0 || !m_jobs.contains(response.request_id)) return; |
|
|
|
|
mf_message response{}; if (mf_decode(data + 16, length - 16, &response) < 0) return; |
|
|
|
|
if (response.request_id == m_listId) { |
|
|
|
|
if (mf_get64(data) != m_galleryRef.group || mf_get64(data + 8) != m_galleryRef.server || |
|
|
|
|
response.owner != m_galleryRef.owner || response.command != MF_LIST || strcmp(response.path, "avatars")) return; |
|
|
|
|
m_listId = 0; |
|
|
|
|
if ((response.status == MF_STALE || response.status == MF_BUSY) && m_listRetries++ < 3) { |
|
|
|
|
m_catalog.clear(); const Ref source = m_galleryRef; |
|
|
|
|
QTimer::singleShot(1000, this, [this, source] { |
|
|
|
|
if (source.group == m_galleryRef.group && source.owner == m_galleryRef.owner && source.server == m_galleryRef.server) listGallery(); |
|
|
|
|
}); return; |
|
|
|
|
} |
|
|
|
|
if (response.status) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: catalog failed status=%s", mf_status_text(response.status)); |
|
|
|
|
emit galleryError(QString::fromUtf8(mf_status_text(response.status))); return; |
|
|
|
|
} |
|
|
|
|
for (unsigned at = 0; at < response.data_len;) { |
|
|
|
|
const uint8_t* p = response.data + at; |
|
|
|
|
if (response.data_len - at < MF_ENTRY_HEADER) { emit galleryError(text("Invalid directory response")); return; } |
|
|
|
|
const unsigned n = p[0] | (unsigned(p[1]) << 8); |
|
|
|
|
if (!n || n > MF_NAME_MAX || n > response.data_len - at - MF_ENTRY_HEADER) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: malformed catalog entry offset=%u", at); |
|
|
|
|
emit galleryError(text("Invalid directory response")); return; |
|
|
|
|
} |
|
|
|
|
const QByteArray file((const char*)p + MF_ENTRY_HEADER, n); |
|
|
|
|
if (p[2] == MF_FILE && mf_avatar_manifest_valid(file.constData()) && mf_get64(p + 3) <= MF_AVATAR_MANIFEST_MAX_BYTES) |
|
|
|
|
m_catalog[QString::fromLatin1(file).left(64)] = mf_get64(p + 11); |
|
|
|
|
at += MF_ENTRY_HEADER + n; |
|
|
|
|
} |
|
|
|
|
if (response.more) { listGallery(response.cursor, QByteArray((const char*)response.version, 16)); return; } |
|
|
|
|
m_gallery = m_catalog.keys(); |
|
|
|
|
std::sort(m_gallery.begin(), m_gallery.end(), [this](const QString& a, const QString& b) { |
|
|
|
|
return m_catalog[a] != m_catalog[b] ? m_catalog[a] > m_catalog[b] : a < b; |
|
|
|
|
}); |
|
|
|
|
if (!m_gallery.contains(m_galleryRef.hash)) m_gallery.prepend(m_galleryRef.hash); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "avatar: catalog ready entries=%d", m_gallery.size()); emit galleryChanged(); return; |
|
|
|
|
} |
|
|
|
|
if (!m_jobs.contains(response.request_id)) return; |
|
|
|
|
auto& job = m_jobs[response.request_id]; |
|
|
|
|
if (job.ref.group != mf_get64(data) || job.ref.server != mf_get64(data + 8) || response.command != job.stage) return; |
|
|
|
|
if (job.generation != m_generation) { finish(job.id, MF_CANCELLED); return; } |
|
|
|
|
if (job.upload && job.generation != m_generation) { finish(job.id, MF_CANCELLED); return; } |
|
|
|
|
if (job.stage == MF_MKDIR && (response.status == MF_OK || response.status == MF_EXISTS)) { post(job, MF_STAT); return; } |
|
|
|
|
if (job.stage == MF_STAT && response.status == MF_NOT_FOUND) { transfer(job); return; } |
|
|
|
|
if (job.stage == MF_STAT && response.status == MF_OK && response.type == MF_FILE && response.size <= AvatarCache::MaxBytes) { |
|
|
|
|
publish(job); return; |
|
|
|
|
if (job.stage == MF_STAT && response.status == MF_OK && response.type == MF_FILE && |
|
|
|
|
response.size == uint64_t(QFileInfo(m_directory + '/' + job.file).size())) { |
|
|
|
|
nextUpload(job); return; |
|
|
|
|
} |
|
|
|
|
finish(job.id, response.status ? response.status : MF_INVALID_RANGE); |
|
|
|
|
} |
|
|
|
|
@ -389,24 +430,35 @@ void AvatarManager::onTransfer(const uint8_t* data, int length) {
|
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: stale or mismatched transfer completion id=%llu", (unsigned long long)id); return; |
|
|
|
|
} |
|
|
|
|
if (status) { finish(id, status); return; } |
|
|
|
|
if (job.upload) { publish(job); return; } |
|
|
|
|
if (job.upload) { nextUpload(job); return; } |
|
|
|
|
const Job snapshot = job; const QString directory = m_directory; job.stage = 101; |
|
|
|
|
QStringList pinned; if (!m_defaultHash.isEmpty()) pinned << m_defaultHash; |
|
|
|
|
for (const auto& active : m_jobs) if (active.upload) pinned << active.ref.hash; |
|
|
|
|
m_pool.start(QRunnable::create([this, snapshot, directory, pinned] { |
|
|
|
|
const auto result = AvatarCache::publish(snapshot.local, directory, snapshot.ref.hash); AvatarCache::prune(directory, pinned); |
|
|
|
|
const auto result = AvatarCache::publish(snapshot.local, directory, snapshot.file); AvatarCache::prune(directory, pinned); |
|
|
|
|
QMetaObject::invokeMethod(this, [this, snapshot, result] { |
|
|
|
|
if (!m_jobs.contains(snapshot.id)) return; |
|
|
|
|
if (result.image.isNull()) { |
|
|
|
|
if (!result.error.isEmpty() || (!snapshot.file.endsWith(".json") && !snapshot.original && result.image.size() != QSize(256, 256))) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: validation failed hash=%s reason=%s", qPrintable(snapshot.ref.hash), qPrintable(result.error)); |
|
|
|
|
finish(snapshot.id, MF_HASH_MISMATCH); return; |
|
|
|
|
} |
|
|
|
|
m_images.insert(result.hash, new QImage(result.image), qMax(1, int(result.image.sizeInBytes() / 1024))); |
|
|
|
|
m_missing.remove(result.hash); finish(snapshot.id, MF_OK); notify(result.hash); |
|
|
|
|
if (snapshot.file.endsWith(".json")) { |
|
|
|
|
m_records[snapshot.ref.hash] = result.record; |
|
|
|
|
auto& job = m_jobs[snapshot.id]; job.file = snapshot.original ? result.record.original : result.record.preview; |
|
|
|
|
transfer(job); return; |
|
|
|
|
} |
|
|
|
|
const QString cacheKey = snapshot.ref.hash + (snapshot.original ? ":original" : ""); |
|
|
|
|
m_images.insert(cacheKey, new QImage(result.image), qMax(1, int(result.image.sizeInBytes() / 1024))); |
|
|
|
|
m_missing.remove(cacheKey); finish(snapshot.id, MF_OK); notify(snapshot.ref.hash); |
|
|
|
|
}, Qt::QueuedConnection); |
|
|
|
|
})); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AvatarManager::nextUpload(Job& job) { |
|
|
|
|
if (++job.object == job.files.size()) { publish(job); return; } |
|
|
|
|
job.file = job.files[job.object]; post(job, MF_STAT); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AvatarManager::onPublished(const uint8_t* data, int length) { |
|
|
|
|
if (length != 17 || !m_jobs.contains(mf_get64(data))) return; |
|
|
|
|
auto& job = m_jobs[mf_get64(data)]; if (job.stage != 100 || job.ref.group != mf_get64(data + 8)) return; |
|
|
|
|
@ -416,19 +468,21 @@ void AvatarManager::onPublished(const uint8_t* data, int length) {
|
|
|
|
|
void AvatarManager::finish(quint64 id, int status) { |
|
|
|
|
const Job job = m_jobs.take(id); const QString context = key(job.ref.group, job.ref.owner); |
|
|
|
|
const Ref current = m_refs.value(context); |
|
|
|
|
if ((job.upload && job.generation != m_generation) || (!job.upload && |
|
|
|
|
if ((job.upload && job.generation != m_generation) || (!job.upload && !job.gallery && |
|
|
|
|
(current.hash != job.ref.hash || current.server != job.ref.server))) { |
|
|
|
|
QTimer::singleShot(0, this, &AvatarManager::pump); return; |
|
|
|
|
} |
|
|
|
|
if (status && status != MF_CANCELLED) { |
|
|
|
|
unsigned attempt = qMin(6u, m_failures.value(context) + 1); m_failures[context] = attempt; |
|
|
|
|
m_retry[context] = QDateTime::currentMSecsSinceEpoch() + (5000LL << (attempt - 1)); |
|
|
|
|
const QString retryKey = job.upload ? context : job.ref.hash + (job.original ? ":original" : ""); |
|
|
|
|
m_retry[retryKey] = QDateTime::currentMSecsSinceEpoch() + (5000LL << (attempt - 1)); |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: operation failed group=%llu owner=%016llx hash=%s status=%s retry_ms=%lld", |
|
|
|
|
(unsigned long long)job.ref.group, (unsigned long long)job.ref.owner, qPrintable(job.ref.hash), mf_status_text(status), |
|
|
|
|
(long long)(5000LL << (attempt - 1))); |
|
|
|
|
if (job.upload && job.generation == m_generation) emit statusChanged(text("Avatar upload failed: %1").arg(mf_status_text(status))); |
|
|
|
|
if (job.gallery) emit galleryError(QString::fromUtf8(mf_status_text(status))); |
|
|
|
|
} else if (!status) { |
|
|
|
|
m_retry.remove(context); m_failures.remove(context); |
|
|
|
|
m_retry.remove(job.upload ? context : job.ref.hash + (job.original ? ":original" : "")); m_failures.remove(context); |
|
|
|
|
if (job.upload && job.generation == m_generation) emit statusChanged(text("Avatar updated")); |
|
|
|
|
} |
|
|
|
|
QTimer::singleShot(0, this, &AvatarManager::pump); |
|
|
|
|
|