20 changed files with 851 additions and 70 deletions
@ -0,0 +1,288 @@ |
|||||||
|
#include "media_blocks.h" |
||||||
|
|
||||||
|
#include "../../lib/debug_config.h" |
||||||
|
|
||||||
|
#include <QDir> |
||||||
|
#include <QFileInfo> |
||||||
|
#include <QDateTime> |
||||||
|
#include <QRandomGenerator> |
||||||
|
|
||||||
|
#include <cmath> |
||||||
|
|
||||||
|
/* ─── MediaBlockReader ─── */ |
||||||
|
|
||||||
|
MediaBlockReader::MediaBlockReader() {} |
||||||
|
|
||||||
|
void MediaBlockReader::initFromAttrs(const QString& mediaDir, |
||||||
|
const QString& dt, const QString& basename, |
||||||
|
const QString& suffix, const QString& ext, |
||||||
|
const QJsonObject& localAttrs, |
||||||
|
qint64 blockSize, qint64 totalSize, int totalBlocks) |
||||||
|
{ |
||||||
|
m_mediaDir = mediaDir; m_dt = dt; m_basename = basename; |
||||||
|
m_suffix = suffix; m_ext = ext; |
||||||
|
m_blockSize = blockSize; m_totalSize = totalSize; m_totalBlocks = totalBlocks; |
||||||
|
m_pos = 0; m_valid = true; |
||||||
|
|
||||||
|
QString st = localAttrs.value("st").toString(); |
||||||
|
if (st == "fl") { |
||||||
|
m_mode = SINGLE_FILE; |
||||||
|
QString fp = localAttrs.value("fp").toString(); |
||||||
|
m_filePath = mediaDir + "/" + fp; |
||||||
|
m_fileSize = totalSize; |
||||||
|
QFileInfo fi(m_filePath); |
||||||
|
m_complete = fi.exists() && fi.size() == totalSize; |
||||||
|
} else { |
||||||
|
m_mode = FRAGMENTED; |
||||||
|
QJsonArray bl = localAttrs.value("bl").toArray(); |
||||||
|
for (const auto& v : bl) m_blocks.append(v.toInt()); |
||||||
|
m_complete = (m_blocks.size() == totalBlocks); |
||||||
|
} |
||||||
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MediaBlockReader: mode=%s complete=%d path=%s blocks=%d/%d", |
||||||
|
m_mode == SINGLE_FILE ? "single" : "frag", m_complete, |
||||||
|
qPrintable(debugPath()), m_blocks.size(), totalBlocks); |
||||||
|
} |
||||||
|
|
||||||
|
MediaBlockReader MediaBlockReader::fromLegacyFile(const QString& filePath) { |
||||||
|
MediaBlockReader r; |
||||||
|
r.m_mode = SINGLE_FILE; |
||||||
|
r.m_filePath = filePath; |
||||||
|
r.m_valid = true; |
||||||
|
QFileInfo fi(filePath); |
||||||
|
r.m_fileSize = fi.size(); |
||||||
|
r.m_complete = fi.exists() && r.m_fileSize > 0; |
||||||
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MediaBlockReader: legacy file=%s size=%lld valid=%d", |
||||||
|
qPrintable(filePath), (long long)r.m_fileSize, r.m_valid); |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
qint64 MediaBlockReader::blockSizeForN(int n) const { |
||||||
|
if (n < m_totalBlocks - 1) return m_blockSize; |
||||||
|
qint64 last = m_totalSize - (m_totalBlocks - 1) * m_blockSize; |
||||||
|
if (last <= 0) last = m_totalSize; |
||||||
|
return last; |
||||||
|
} |
||||||
|
|
||||||
|
QString MediaBlockReader::blockFilePath(int n) const { |
||||||
|
return QString("%1/%2_%3_%4_%5.%6") |
||||||
|
.arg(m_mediaDir, m_dt).arg(n).arg(m_basename, m_suffix, m_ext); |
||||||
|
} |
||||||
|
|
||||||
|
QByteArray MediaBlockReader::readAll() { |
||||||
|
if (!m_valid || !m_complete || m_mode == NONE) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "MediaBlockReader::readAll: invalid/incomplete"); |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
if (m_mode == SINGLE_FILE) { |
||||||
|
QFile f(m_filePath); |
||||||
|
if (!f.open(QIODevice::ReadOnly)) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "MediaBlockReader::readAll: cannot open %s", qPrintable(m_filePath)); |
||||||
|
return {}; |
||||||
|
} |
||||||
|
QByteArray data = f.readAll(); |
||||||
|
m_pos = data.size(); |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
QByteArray data; data.resize(m_totalSize); |
||||||
|
qint64 offset = 0; |
||||||
|
for (int i = 0; i < m_blocks.size(); i++) { |
||||||
|
int n = m_blocks[i]; |
||||||
|
QString path = blockFilePath(n); |
||||||
|
QFile f(path); |
||||||
|
if (!f.open(QIODevice::ReadOnly)) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "MediaBlockReader::readAll: cannot open block %d: %s", n, qPrintable(path)); |
||||||
|
return {}; |
||||||
|
} |
||||||
|
qint64 sz = blockSizeForN(n); |
||||||
|
f.read(data.data() + offset, sz); |
||||||
|
offset += sz; |
||||||
|
} |
||||||
|
m_pos = data.size(); |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
QByteArray MediaBlockReader::readRange(qint64 offset, qint64 size) { |
||||||
|
if (!m_valid || offset < 0 || size <= 0 || m_mode == NONE) return {}; |
||||||
|
if (offset >= m_totalSize) return {}; |
||||||
|
|
||||||
|
if (offset + size > m_totalSize) size = m_totalSize - offset; |
||||||
|
|
||||||
|
if (m_mode == SINGLE_FILE) { |
||||||
|
QFile f(m_filePath); |
||||||
|
if (!f.open(QIODevice::ReadOnly)) return {}; |
||||||
|
f.seek(offset); |
||||||
|
QByteArray data = f.read(size); |
||||||
|
m_pos = offset + data.size(); |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
int startBlock = offset / m_blockSize; |
||||||
|
QByteArray data; data.resize(size); |
||||||
|
qint64 copied = 0; |
||||||
|
qint64 remaining = size; |
||||||
|
int currentN = startBlock; |
||||||
|
|
||||||
|
while (remaining > 0 && currentN < m_totalBlocks && m_blocks.contains(currentN)) { |
||||||
|
qint64 blockStart = (qint64)currentN * m_blockSize; |
||||||
|
qint64 blockSize = blockSizeForN(currentN); |
||||||
|
qint64 inBlockOffset = offset + copied - blockStart; |
||||||
|
if (inBlockOffset < 0) inBlockOffset = 0; |
||||||
|
qint64 toRead = qMin(remaining, blockSize - inBlockOffset); |
||||||
|
if (toRead <= 0) break; |
||||||
|
|
||||||
|
QString path = blockFilePath(currentN); |
||||||
|
QFile f(path); |
||||||
|
if (f.open(QIODevice::ReadOnly)) { |
||||||
|
f.seek(inBlockOffset); |
||||||
|
f.read(data.data() + copied, toRead); |
||||||
|
} |
||||||
|
copied += toRead; |
||||||
|
remaining -= toRead; |
||||||
|
currentN++; |
||||||
|
} |
||||||
|
m_pos = offset + copied; |
||||||
|
if (copied < size) data.resize(copied); |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
bool MediaBlockReader::seek(qint64 pos) { |
||||||
|
if (!m_valid || pos < 0 || pos > m_totalSize) return false; |
||||||
|
m_pos = pos; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
bool MediaBlockReader::atEnd() const { |
||||||
|
return m_pos >= m_totalSize; |
||||||
|
} |
||||||
|
|
||||||
|
bool MediaBlockReader::saveToFile(const QString& destPath) { |
||||||
|
if (!m_valid || !m_complete) return false; |
||||||
|
|
||||||
|
QByteArray data = readAll(); |
||||||
|
if (data.isEmpty()) return false; |
||||||
|
|
||||||
|
QFile f(destPath); |
||||||
|
if (!f.open(QIODevice::WriteOnly)) return false; |
||||||
|
f.write(data); |
||||||
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MediaBlockReader: saved to %s (%lld bytes)", qPrintable(destPath), (long long)data.size()); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
QString MediaBlockReader::assembleAndCleanup() { |
||||||
|
if (!m_valid || m_mode != FRAGMENTED || !m_complete) return {}; |
||||||
|
|
||||||
|
QString assembledPath = QString("%1/%2_%3_%4.%5") |
||||||
|
.arg(m_mediaDir, m_dt, m_basename, m_suffix, m_ext); |
||||||
|
|
||||||
|
QFile af(assembledPath); |
||||||
|
if (!af.open(QIODevice::WriteOnly)) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "MediaBlockReader::assemble: cannot create %s", qPrintable(assembledPath)); |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
for (int i = 0; i < m_blocks.size(); i++) { |
||||||
|
int n = m_blocks[i]; |
||||||
|
QString path = blockFilePath(n); |
||||||
|
QFile bf(path); |
||||||
|
if (bf.open(QIODevice::ReadOnly)) { |
||||||
|
af.write(bf.readAll()); |
||||||
|
bf.close(); |
||||||
|
QFile::remove(path); |
||||||
|
} |
||||||
|
} |
||||||
|
af.close(); |
||||||
|
|
||||||
|
m_mode = SINGLE_FILE; |
||||||
|
m_filePath = assembledPath; |
||||||
|
m_fileSize = m_totalSize; |
||||||
|
m_blocks.clear(); |
||||||
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MediaBlockReader: assembled %s", qPrintable(assembledPath)); |
||||||
|
return assembledPath; |
||||||
|
} |
||||||
|
|
||||||
|
QJsonObject MediaBlockReader::localAttrsAfterAssembly() const { |
||||||
|
QJsonObject obj; |
||||||
|
obj["st"] = "fl"; |
||||||
|
obj["fp"] = QString("%1_%2_%3.%4").arg(m_dt, m_basename, m_suffix, m_ext); |
||||||
|
QJsonArray bl; |
||||||
|
for (int i = 0; i < m_totalBlocks; i++) bl.append(i); |
||||||
|
obj["bl"] = bl; |
||||||
|
return obj; |
||||||
|
} |
||||||
|
|
||||||
|
QString MediaBlockReader::debugPath() const { |
||||||
|
if (m_mode == SINGLE_FILE) return m_filePath; |
||||||
|
if (m_mode == FRAGMENTED) return QString("%1/%2_*_%3_%4.%5") |
||||||
|
.arg(m_mediaDir, m_dt, m_basename, m_suffix, m_ext); |
||||||
|
return "none"; |
||||||
|
} |
||||||
|
|
||||||
|
/* ─── media_split_to_blocks ─── */ |
||||||
|
|
||||||
|
qint64 media_calc_block_size(qint64 fileSize) { |
||||||
|
if (fileSize <= 0) return 0; |
||||||
|
qint64 target = (fileSize + 29) / 30; /* ceil(file_size / 30) */ |
||||||
|
if (target < 10 * 1024 * 1024) target = 10 * 1024 * 1024; |
||||||
|
if (target > 25 * 1024 * 1024) target = 25 * 1024 * 1024; |
||||||
|
return target; |
||||||
|
} |
||||||
|
|
||||||
|
MediaBlockInfo media_split_to_blocks(const QString& srcFile, |
||||||
|
const QString& mediaDir, |
||||||
|
bool removeSrc) |
||||||
|
{ |
||||||
|
MediaBlockInfo info; |
||||||
|
QFileInfo fi(srcFile); |
||||||
|
if (!fi.exists()) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: src not found: %s", qPrintable(srcFile)); |
||||||
|
return info; |
||||||
|
} |
||||||
|
|
||||||
|
info.fileSize = fi.size(); |
||||||
|
if (info.fileSize == 0) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: empty file: %s", qPrintable(srcFile)); |
||||||
|
return info; |
||||||
|
} |
||||||
|
|
||||||
|
info.blockSize = media_calc_block_size(info.fileSize); |
||||||
|
info.numBlocks = (int)((info.fileSize + info.blockSize - 1) / info.blockSize); |
||||||
|
info.ext = fi.suffix(); |
||||||
|
info.basename = fi.completeBaseName(); |
||||||
|
|
||||||
|
QDateTime now = QDateTime::currentDateTimeUtc(); |
||||||
|
info.dt = now.toString("yyyyMMdd-HHmmss"); |
||||||
|
uint16_t rnd = QRandomGenerator::global()->generate() & 0xFFFF; |
||||||
|
info.suffix = QString::number(rnd, 16).rightJustified(4, '0'); |
||||||
|
|
||||||
|
QDir().mkpath(mediaDir); |
||||||
|
|
||||||
|
QFile src(srcFile); |
||||||
|
if (!src.open(QIODevice::ReadOnly)) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: cannot open src: %s", qPrintable(srcFile)); |
||||||
|
return info; |
||||||
|
} |
||||||
|
|
||||||
|
qint64 remaining = info.fileSize; |
||||||
|
for (int n = 0; n < info.numBlocks; n++) { |
||||||
|
qint64 chunk = qMin(remaining, info.blockSize); |
||||||
|
QString path = QString("%1/%2_%3_%4_%5.%6") |
||||||
|
.arg(mediaDir, info.dt).arg(n).arg(info.basename, info.suffix, info.ext); |
||||||
|
QFile dst(path); |
||||||
|
if (!dst.open(QIODevice::WriteOnly)) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: cannot create block %d: %s", n, qPrintable(path)); |
||||||
|
src.close(); return info; |
||||||
|
} |
||||||
|
dst.write(src.read(chunk)); |
||||||
|
dst.close(); |
||||||
|
remaining -= chunk; |
||||||
|
} |
||||||
|
src.close(); |
||||||
|
|
||||||
|
if (removeSrc) QFile::remove(srcFile); |
||||||
|
|
||||||
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: %s -> %d blocks (%lld bytes each) in %s", |
||||||
|
qPrintable(fi.fileName()), info.numBlocks, (long long)info.blockSize, qPrintable(mediaDir)); |
||||||
|
return info; |
||||||
|
} |
||||||
@ -0,0 +1,72 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <QString> |
||||||
|
#include <QList> |
||||||
|
#include <QFile> |
||||||
|
#include <QJsonObject> |
||||||
|
#include <cstdint> |
||||||
|
|
||||||
|
class MediaBlockReader { |
||||||
|
public: |
||||||
|
MediaBlockReader(); |
||||||
|
|
||||||
|
void initFromAttrs(const QString& mediaDir, |
||||||
|
const QString& dt, const QString& basename, |
||||||
|
const QString& suffix, const QString& ext, |
||||||
|
const QJsonObject& localAttrs, |
||||||
|
qint64 blockSize, qint64 totalSize, int totalBlocks); |
||||||
|
|
||||||
|
static MediaBlockReader fromLegacyFile(const QString& filePath); |
||||||
|
|
||||||
|
bool isValid() const { return m_valid; } |
||||||
|
bool isComplete() const { return m_complete; } |
||||||
|
bool isSingleFile() const { return m_mode == SINGLE_FILE; } |
||||||
|
bool isFragmented() const { return m_mode == FRAGMENTED; } |
||||||
|
qint64 totalSize() const { return m_totalSize; } |
||||||
|
|
||||||
|
QByteArray readAll(); |
||||||
|
QByteArray readRange(qint64 offset, qint64 size); |
||||||
|
bool seek(qint64 pos); |
||||||
|
qint64 pos() const { return m_pos; } |
||||||
|
bool atEnd() const; |
||||||
|
|
||||||
|
bool saveToFile(const QString& destPath); |
||||||
|
QString assembleAndCleanup(); |
||||||
|
QJsonObject localAttrsAfterAssembly() const; |
||||||
|
|
||||||
|
QString debugPath() const; |
||||||
|
|
||||||
|
private: |
||||||
|
enum Mode { NONE, SINGLE_FILE, FRAGMENTED }; |
||||||
|
Mode m_mode = NONE; |
||||||
|
bool m_valid = false; |
||||||
|
bool m_complete = false; |
||||||
|
qint64 m_pos = 0; |
||||||
|
|
||||||
|
QString m_filePath; |
||||||
|
qint64 m_fileSize = 0; |
||||||
|
|
||||||
|
QString m_mediaDir, m_dt, m_basename, m_suffix, m_ext; |
||||||
|
QList<int> m_blocks; |
||||||
|
qint64 m_blockSize = 0; |
||||||
|
qint64 m_totalSize = 0; |
||||||
|
int m_totalBlocks = 0; |
||||||
|
|
||||||
|
qint64 blockSizeForN(int n) const; |
||||||
|
QString blockFilePath(int n) const; |
||||||
|
}; |
||||||
|
|
||||||
|
struct MediaBlockInfo { |
||||||
|
QString dt; |
||||||
|
QString basename; |
||||||
|
QString suffix; |
||||||
|
QString ext; |
||||||
|
int numBlocks = 0; |
||||||
|
qint64 fileSize = 0; |
||||||
|
qint64 blockSize = 0; |
||||||
|
}; |
||||||
|
|
||||||
|
qint64 media_calc_block_size(qint64 fileSize); |
||||||
|
MediaBlockInfo media_split_to_blocks(const QString& srcFile, |
||||||
|
const QString& mediaDir, |
||||||
|
bool removeSrc = true); |
||||||
Loading…
Reference in new issue