[WIP] Zipper to download artists/releases, ref #37
This commit is contained in:
@@ -115,6 +115,7 @@
|
|||||||
<message id="Lms.Explore.add-filter">Add filter</message>
|
<message id="Lms.Explore.add-filter">Add filter</message>
|
||||||
<message id="Lms.Explore.all">All</message>
|
<message id="Lms.Explore.all">All</message>
|
||||||
<message id="Lms.Explore.artists">Artists</message>
|
<message id="Lms.Explore.artists">Artists</message>
|
||||||
|
<message id="Lms.Explore.download">Download</message>
|
||||||
<message id="Lms.Explore.filter-added">Filter added</message>
|
<message id="Lms.Explore.filter-added">Filter added</message>
|
||||||
<message id="Lms.Explore.filters">Filters</message>
|
<message id="Lms.Explore.filters">Filters</message>
|
||||||
<message id="Lms.Explore.links">Links</message>
|
<message id="Lms.Explore.links">Links</message>
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
<message id="Lms.Explore.add-filter">Ajouter filtre</message>
|
<message id="Lms.Explore.add-filter">Ajouter filtre</message>
|
||||||
<message id="Lms.Explore.all">Tous</message>
|
<message id="Lms.Explore.all">Tous</message>
|
||||||
<message id="Lms.Explore.artists">Artistes</message>
|
<message id="Lms.Explore.artists">Artistes</message>
|
||||||
|
<message id="Lms.Explore.download">Télécharger</message>
|
||||||
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
|
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
|
||||||
<message id="Lms.Explore.filters">Filtres</message>
|
<message id="Lms.Explore.filters">Filtres</message>
|
||||||
<message id="Lms.Explore.links">Liens</message>
|
<message id="Lms.Explore.links">Liens</message>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ add_library(lmsutils SHARED
|
|||||||
impl/String.cpp
|
impl/String.cpp
|
||||||
impl/UUID.cpp
|
impl/UUID.cpp
|
||||||
impl/WtLogger.cpp
|
impl/WtLogger.cpp
|
||||||
|
impl/Zipper.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(lmsutils INTERFACE
|
target_include_directories(lmsutils INTERFACE
|
||||||
|
|||||||
@@ -32,14 +32,13 @@
|
|||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
void
|
std::uint32_t
|
||||||
computeCrc(const std::filesystem::path& p, std::vector<unsigned char>& crc)
|
computeCrc32(const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
using crc_type = boost::crc_32_type;
|
using crc_type = boost::crc_32_type;
|
||||||
crc_type result;
|
crc_type result;
|
||||||
|
|
||||||
std::ifstream ifs( p.string().c_str(), std::ios_base::binary );
|
std::ifstream ifs {p.string().c_str(), std::ios_base::binary};
|
||||||
|
|
||||||
if (ifs)
|
if (ifs)
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@@ -49,7 +48,7 @@ computeCrc(const std::filesystem::path& p, std::vector<unsigned char>& crc)
|
|||||||
ifs.read( buffer.data(), buffer.size() );
|
ifs.read( buffer.data(), buffer.size() );
|
||||||
result.process_bytes( buffer.data(), ifs.gcount() );
|
result.process_bytes( buffer.data(), ifs.gcount() );
|
||||||
}
|
}
|
||||||
while ( ifs );
|
while (ifs);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -58,13 +57,7 @@ computeCrc(const std::filesystem::path& p, std::vector<unsigned char>& crc)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Copy the result into a vector of unsigned char
|
return result.checksum();
|
||||||
const crc_type::value_type checksum = result.checksum();
|
|
||||||
for (std::size_t i = 0; (i+1)*8 <= crc_type::bit_count; i++)
|
|
||||||
{
|
|
||||||
const unsigned char* data = reinterpret_cast<const unsigned char*>( &checksum );
|
|
||||||
crc.push_back(data[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
|||||||
@@ -0,0 +1,499 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "utils/Zipper.hpp"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
#include "utils/Path.hpp"
|
||||||
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
|
// Done using specs from https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||||||
|
|
||||||
|
namespace Zip
|
||||||
|
{
|
||||||
|
|
||||||
|
class ZipHeader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ZipHeader(std::byte* buffer, std::size_t bufferSize)
|
||||||
|
: _buffer {buffer}
|
||||||
|
, _bufferSize {bufferSize}
|
||||||
|
{}
|
||||||
|
|
||||||
|
enum GeneralPurposeFlag : std::uint16_t
|
||||||
|
{
|
||||||
|
UseDataDescriptor = 1 << 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum CompressionMethod : std::uint16_t
|
||||||
|
{
|
||||||
|
NoCompression = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void write8(std::size_t offset, std::uint8_t value);
|
||||||
|
void write16(std::size_t offset, std::uint16_t value);
|
||||||
|
void write32(std::size_t offset, std::uint32_t value);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::byte* _buffer {};
|
||||||
|
std::size_t _bufferSize {};
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
ZipHeader::write8(std::size_t offset, std::uint8_t value)
|
||||||
|
{
|
||||||
|
_buffer[offset] = static_cast<std::byte>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ZipHeader::write16(std::size_t offset, std::uint16_t value)
|
||||||
|
{
|
||||||
|
_buffer[offset] = static_cast<std::byte>(value & 0xff);
|
||||||
|
_buffer[offset + 1] = static_cast<std::byte>(value >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ZipHeader::write32(std::size_t offset, std::uint32_t value)
|
||||||
|
{
|
||||||
|
_buffer[offset] = static_cast<std::byte>(value & 0xff);
|
||||||
|
_buffer[offset + 1] = static_cast<std::byte>((value >> 8) & 0xff);
|
||||||
|
_buffer[offset + 2] = static_cast<std::byte>((value >> 16) & 0xff);
|
||||||
|
_buffer[offset + 3] = static_cast<std::byte>(value >> 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalFileHeader : public ZipHeader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ZipHeader::ZipHeader;
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
void setSignature();
|
||||||
|
void setVersionNeededToExtract(unsigned major, unsigned minor);
|
||||||
|
void setGeneralPurposeFlags(std::uint16_t flags);
|
||||||
|
void setCompressionMethod(CompressionMethod compressionMethod);
|
||||||
|
void setLastModifiedDateTime();
|
||||||
|
void setCrc32UncompressedData(std::uint32_t crc);
|
||||||
|
void setCompressedSize(std::size_t size);
|
||||||
|
void setUncompressedSize(std::size_t size);
|
||||||
|
void setFileNameLength(std::size_t size);
|
||||||
|
void setExtraFieldLength(std::size_t size);
|
||||||
|
static constexpr std::size_t getHeaderSize() { return 30; }
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setSignature()
|
||||||
|
{
|
||||||
|
write32(0, 0x04034b50);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setVersionNeededToExtract(unsigned major, unsigned minor)
|
||||||
|
{
|
||||||
|
assert(minor < 10);
|
||||||
|
write16(4, major*10 + minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setGeneralPurposeFlags(std::uint16_t flags)
|
||||||
|
{
|
||||||
|
write16(6, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setCompressionMethod(CompressionMethod compressionMethod)
|
||||||
|
{
|
||||||
|
write16(8, compressionMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setLastModifiedDateTime()
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
write16(10, 0); // time
|
||||||
|
write16(12, 0); // date
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setCrc32UncompressedData(std::uint32_t crc)
|
||||||
|
{
|
||||||
|
write32(14, crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setCompressedSize(std::size_t size)
|
||||||
|
{
|
||||||
|
write32(18, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setUncompressedSize(std::size_t size)
|
||||||
|
{
|
||||||
|
write32(22, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setFileNameLength(std::size_t size)
|
||||||
|
{
|
||||||
|
write16(26, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
LocalFileHeader::setExtraFieldLength(std::size_t size)
|
||||||
|
{
|
||||||
|
write16(28, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CentralDirectoryHeader : public ZipHeader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ZipHeader::ZipHeader;
|
||||||
|
|
||||||
|
void setSignature() { write32(0, 0x02014b50); }
|
||||||
|
void setVersionMadeBy(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major * 10 + minor); }
|
||||||
|
void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(6, major*10 + minor); }
|
||||||
|
void setGeneralPurposeFlags(std::uint16_t flags) { write16(8, flags); }
|
||||||
|
void setCompressionMethod(CompressionMethod method) { write16(10, method); }
|
||||||
|
void setLastModifiedDateTime()
|
||||||
|
{
|
||||||
|
write16(12, 0); // time
|
||||||
|
write16(14, 0); // date
|
||||||
|
}
|
||||||
|
void setCrc32UncompressedData(std::uint32_t crc32) { write32(16, crc32); }
|
||||||
|
void setCompressedSize(std::size_t size) { write32(20, size); }
|
||||||
|
void setUncompressedSize(std::size_t size) { write32(24, size); }
|
||||||
|
void setFileNameLength(std::size_t size) { write16(28, size); }
|
||||||
|
void setExtraFieldLength(std::size_t size) { write16(30, size); }
|
||||||
|
void setFileCommentLength(std::size_t size) { write16(32, size); }
|
||||||
|
void setDiskNumber(std::size_t number) { write16(34, number); }
|
||||||
|
void setInternalFileAttributes(std::uint16_t attributes) { write16(36, attributes); }
|
||||||
|
void setExternalFileAttributes(std::uint16_t attributes) { write32(38, attributes); }
|
||||||
|
void setRelativeFileHeaderOffset(std::size_t offset) { write32(42, offset); }
|
||||||
|
static constexpr std::size_t getHeaderSize() { return 46; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class EndOfCentralDirectoryRecord : public ZipHeader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ZipHeader::ZipHeader;
|
||||||
|
|
||||||
|
void setSignature() { write32(0, 0x06054b50); }
|
||||||
|
void setDiskNumber(unsigned number) { write16(4, number); }
|
||||||
|
void setCentralDirectoryDiskNumber(unsigned number) { write16(6, number); }
|
||||||
|
void setNbDiskCentralDirectoryRecords(unsigned number) { write16(8, number); }
|
||||||
|
void setNbCentralDirectoryRecords(unsigned number) { write16(10, number); }
|
||||||
|
void setCentralDirectorySize(std::size_t size) { write32(12, size); }
|
||||||
|
void setCentralDirectoryOffset(std::size_t offset) { write32(16, offset); }
|
||||||
|
void setCommentLength(std::size_t length) { write16(20, length); }
|
||||||
|
static constexpr std::size_t getHeaderSize() { return 22; }
|
||||||
|
};
|
||||||
|
|
||||||
|
Zipper::Zipper(const std::map<std::string, std::filesystem::path>& files, CompressionMethod compMethod)
|
||||||
|
: _compMethod {compMethod}
|
||||||
|
{
|
||||||
|
for (const auto& [filename, filePath] : files)
|
||||||
|
{
|
||||||
|
FileContext fileContext;
|
||||||
|
fileContext.filePath = filePath;
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
fileContext.fileSize = std::filesystem::file_size(filePath, ec);
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
LMS_LOG(UTILS, INFO) << "Cannot get file size for '" << filePath.string() << "': " << ec.message();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fileContext.fileCrc32 = computeCrc32(filePath);
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, DEBUG) << "Processing '" << filePath.string() << "': File size = " << fileContext.fileSize;
|
||||||
|
|
||||||
|
_files[filename] = std::move(fileContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentFile = std::begin(_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Zipper::writeSome(std::byte* buffer, std::size_t bufferSize)
|
||||||
|
{
|
||||||
|
// make sure we have some room for the headers
|
||||||
|
assert(bufferSize >= minOutputBufferSize);
|
||||||
|
|
||||||
|
std::size_t nbTotalWrittenBytes {};
|
||||||
|
|
||||||
|
while (!isComplete() && (bufferSize >= minOutputBufferSize))
|
||||||
|
{
|
||||||
|
std::size_t nbWrittenBytes {};
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, DEBUG) << "Global offset = " << _currentZipOffset;
|
||||||
|
LMS_LOG(UTILS, DEBUG) << "Buffer ptr = " << buffer << ", remaining size = " << bufferSize;
|
||||||
|
|
||||||
|
switch (_writeState)
|
||||||
|
{
|
||||||
|
case WriteState::LocalFileHeader:
|
||||||
|
nbWrittenBytes = writeLocalFileHeader(buffer, bufferSize);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WriteState::LocalFileHeaderFileName:
|
||||||
|
nbWrittenBytes = writeLocalFileHeaderFileName(buffer, bufferSize);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WriteState::FileData:
|
||||||
|
nbWrittenBytes = writeFileData(buffer, bufferSize);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WriteState::CentralDirectoryHeader:
|
||||||
|
nbWrittenBytes = writeCentralDirectoryHeader(buffer, bufferSize);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WriteState::CentralDirectoryHeaderFileName:
|
||||||
|
nbWrittenBytes = writeCentralDirectoryHeaderFileName(buffer, bufferSize);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WriteState::EndOfCentralDirectoryRecord:
|
||||||
|
nbWrittenBytes = writeEndOfCentralDirectoryRecord(buffer, bufferSize);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WriteState::Complete:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(UI, DEBUG) << "nbWrittenBytes = " << nbWrittenBytes;
|
||||||
|
|
||||||
|
buffer += nbWrittenBytes;
|
||||||
|
bufferSize -= nbWrittenBytes;
|
||||||
|
_currentZipOffset += nbWrittenBytes;
|
||||||
|
nbTotalWrittenBytes += nbWrittenBytes ;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nbTotalWrittenBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
Zipper::isComplete() const
|
||||||
|
{
|
||||||
|
return _writeState == WriteState::Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Zipper::writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize)
|
||||||
|
{
|
||||||
|
static_assert(LocalFileHeader::getHeaderSize() <= minOutputBufferSize);
|
||||||
|
|
||||||
|
assert(bufferSize >= minOutputBufferSize);
|
||||||
|
|
||||||
|
if (_currentFile == std::cend(_files))
|
||||||
|
{
|
||||||
|
_currentFile = std::begin(_files);
|
||||||
|
_writeState = WriteState::CentralDirectoryHeader;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, INFO) << "writeLocalFileHeader. crc = " << _currentFile->second.fileCrc32;
|
||||||
|
LocalFileHeader header {buffer, bufferSize};
|
||||||
|
|
||||||
|
header.setSignature();
|
||||||
|
header.setVersionNeededToExtract(1, 0);
|
||||||
|
header.setGeneralPurposeFlags(0);
|
||||||
|
header.setCrc32UncompressedData(_currentFile->second.fileCrc32);
|
||||||
|
switch (_compMethod)
|
||||||
|
{
|
||||||
|
case CompressionMethod::NoCompression:
|
||||||
|
header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression);
|
||||||
|
header.setCompressedSize(_currentFile->second.fileSize);
|
||||||
|
header.setUncompressedSize(_currentFile->second.fileSize);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
header.setLastModifiedDateTime(); // getLastWriteTime(*_currentFile));
|
||||||
|
header.setFileNameLength(_currentFile->first.size());
|
||||||
|
header.setExtraFieldLength(0);
|
||||||
|
|
||||||
|
_writeState = WriteState::LocalFileHeaderFileName;
|
||||||
|
_currentFile->second.localFileHeaderOffset = _currentZipOffset;
|
||||||
|
|
||||||
|
return header.getHeaderSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Zipper::writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize)
|
||||||
|
{
|
||||||
|
const std::string& fileName {_currentFile->first};
|
||||||
|
|
||||||
|
assert(_currentOffset <= fileName.size());
|
||||||
|
if (_currentOffset == fileName.size())
|
||||||
|
{
|
||||||
|
_writeState = WriteState::FileData;
|
||||||
|
_currentOffset = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, INFO) << "writeLocalFileHeaderFileName";
|
||||||
|
|
||||||
|
const std::size_t nbBytesToCopy {std::min<std::size_t>(fileName.size() - _currentOffset, bufferSize)};
|
||||||
|
LMS_LOG(UTILS, INFO) << "\tnbBytesToCopy = " << nbBytesToCopy;
|
||||||
|
|
||||||
|
std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast<unsigned char*>(buffer));
|
||||||
|
|
||||||
|
_currentOffset += nbBytesToCopy;
|
||||||
|
return nbBytesToCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Zipper::writeFileData(std::byte* buffer, std::size_t bufferSize)
|
||||||
|
{
|
||||||
|
if (_currentOffset == _currentFile->second.fileSize)
|
||||||
|
{
|
||||||
|
_currentOffset = 0;
|
||||||
|
++_currentFile;
|
||||||
|
_writeState = WriteState::LocalFileHeader;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string filePath {_currentFile->second.filePath.string()};
|
||||||
|
|
||||||
|
std::ifstream ifs {filePath.c_str(), std::ios_base::binary};
|
||||||
|
if (!ifs)
|
||||||
|
throw ZipperException {"File '" + filePath + "' does no longer exist!"};
|
||||||
|
|
||||||
|
ifs.seekg(0, std::ios::end);
|
||||||
|
const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())};
|
||||||
|
ifs.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
if (fileSize != _currentFile->second.fileSize)
|
||||||
|
throw ZipperException {"File '" + filePath + "': size mismatch!"};
|
||||||
|
|
||||||
|
const std::size_t nbBytesToRead {std::min(fileSize - _currentOffset, bufferSize)};
|
||||||
|
|
||||||
|
ifs.seekg(_currentOffset, std::ios::beg);
|
||||||
|
ifs.read(reinterpret_cast<char*>(buffer), nbBytesToRead );
|
||||||
|
const ::uint64_t actualReadSize {static_cast<::uint64_t>(ifs.gcount())};
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, INFO) << "writeFileData: to read = " << nbBytesToRead << ", actually read = " << actualReadSize;
|
||||||
|
|
||||||
|
_currentOffset += actualReadSize;
|
||||||
|
|
||||||
|
return actualReadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Zipper::writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize)
|
||||||
|
{
|
||||||
|
assert(bufferSize >= minOutputBufferSize);
|
||||||
|
static_assert(CentralDirectoryHeader::getHeaderSize() <= minOutputBufferSize);
|
||||||
|
|
||||||
|
if (_currentFile == std::begin(_files))
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, INFO) << "First record! _currentZipOffset = " << _currentZipOffset;
|
||||||
|
_centralDirectoryOffset = _currentZipOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentFile == std::end(_files))
|
||||||
|
{
|
||||||
|
_writeState = WriteState::EndOfCentralDirectoryRecord;
|
||||||
|
_currentFile = std::begin(_files);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, INFO) << "writeCentralDirectoryHeader. Relative offset = " << _currentFile->second.localFileHeaderOffset << ", crc = " << std::hex << _currentFile->second.fileCrc32;
|
||||||
|
|
||||||
|
CentralDirectoryHeader header {buffer, bufferSize};
|
||||||
|
header.setSignature();
|
||||||
|
header.setVersionMadeBy(2, 0);
|
||||||
|
header.setVersionNeededToExtract(1, 0);
|
||||||
|
header.setGeneralPurposeFlags(0);
|
||||||
|
switch (_compMethod)
|
||||||
|
{
|
||||||
|
case CompressionMethod::NoCompression:
|
||||||
|
header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression);
|
||||||
|
header.setCompressedSize(_currentFile->second.fileSize);
|
||||||
|
header.setUncompressedSize(_currentFile->second.fileSize);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
header.setLastModifiedDateTime(); // getLastWriteTime(*_currentFile));
|
||||||
|
header.setCrc32UncompressedData(_currentFile->second.fileCrc32);
|
||||||
|
header.setFileNameLength(_currentFile->first.size());
|
||||||
|
header.setExtraFieldLength(0);
|
||||||
|
header.setFileCommentLength(0);
|
||||||
|
header.setDiskNumber(0);
|
||||||
|
header.setInternalFileAttributes(0);
|
||||||
|
header.setExternalFileAttributes(0);
|
||||||
|
header.setRelativeFileHeaderOffset(_currentFile->second.localFileHeaderOffset);
|
||||||
|
|
||||||
|
_writeState = WriteState::CentralDirectoryHeaderFileName;
|
||||||
|
_centralDirectorySize += header.getHeaderSize();
|
||||||
|
|
||||||
|
return header.getHeaderSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Zipper::writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize)
|
||||||
|
{
|
||||||
|
const std::string& fileName {_currentFile->first};
|
||||||
|
|
||||||
|
assert(_currentOffset <= fileName.size());
|
||||||
|
if (_currentOffset == fileName.size())
|
||||||
|
{
|
||||||
|
_currentOffset = 0;
|
||||||
|
++_currentFile;
|
||||||
|
_writeState = WriteState::CentralDirectoryHeader;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, INFO) << "writeCentralDirectoryHeaderFileName";
|
||||||
|
const std::size_t nbBytesToCopy {std::min<std::size_t>(fileName.size() - _currentOffset, bufferSize)};
|
||||||
|
|
||||||
|
std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast<unsigned char*>(buffer));
|
||||||
|
|
||||||
|
_currentOffset += nbBytesToCopy;
|
||||||
|
_centralDirectorySize += nbBytesToCopy;
|
||||||
|
return nbBytesToCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Zipper::writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize)
|
||||||
|
{
|
||||||
|
assert(bufferSize >= minOutputBufferSize);
|
||||||
|
static_assert(EndOfCentralDirectoryRecord::getHeaderSize() <= minOutputBufferSize);
|
||||||
|
|
||||||
|
EndOfCentralDirectoryRecord record {buffer, bufferSize};
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, DEBUG) << "Writing EOR. nb records = " << _files.size() << ", offset = " << _centralDirectoryOffset << ", size = " << _centralDirectorySize;
|
||||||
|
|
||||||
|
record.setSignature();
|
||||||
|
record.setDiskNumber(0);
|
||||||
|
record.setCentralDirectoryDiskNumber(0);
|
||||||
|
record.setNbDiskCentralDirectoryRecords(_files.size());
|
||||||
|
record.setNbCentralDirectoryRecords(_files.size());
|
||||||
|
record.setCentralDirectorySize(_centralDirectorySize);
|
||||||
|
record.setCentralDirectoryOffset(_centralDirectoryOffset);
|
||||||
|
record.setCommentLength(0);
|
||||||
|
|
||||||
|
_writeState = WriteState::Complete;
|
||||||
|
return record.getHeaderSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Zip
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
void computeCrc(const std::filesystem::path& p, std::vector<unsigned char>& checksum);
|
std::uint32_t computeCrc32(const std::filesystem::path& p);
|
||||||
|
|
||||||
// Make sure the given path is a directory
|
// Make sure the given path is a directory
|
||||||
// Create it if needed
|
// Create it if needed
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ template<>
|
|||||||
std::optional<std::string>
|
std::optional<std::string>
|
||||||
readAs(const std::string& str);
|
readAs(const std::string& str);
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
std::string
|
std::string
|
||||||
replaceInString(const std::string& str, const std::string& from, const std::string& to);
|
replaceInString(const std::string& str, const std::string& from, const std::string& to);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "Exception.hpp"
|
||||||
|
|
||||||
|
namespace Zip
|
||||||
|
{
|
||||||
|
|
||||||
|
class ZipperException : public LmsException
|
||||||
|
{
|
||||||
|
using LmsException::LmsException;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Very simple on-the-fly zip creator
|
||||||
|
class Zipper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
enum class CompressionMethod
|
||||||
|
{
|
||||||
|
NoCompression,
|
||||||
|
};
|
||||||
|
|
||||||
|
Zipper(const std::map<std::string, std::filesystem::path>& files, CompressionMethod comp = CompressionMethod::NoCompression);
|
||||||
|
|
||||||
|
static constexpr std::size_t minOutputBufferSize = 64;
|
||||||
|
std::size_t writeSome(std::byte* buffer, std::size_t bufferSize);
|
||||||
|
bool isComplete() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setComplete();
|
||||||
|
|
||||||
|
std::size_t writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize);
|
||||||
|
std::size_t writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize);
|
||||||
|
std::size_t writeFileData(std::byte* buffer, std::size_t bufferSize);
|
||||||
|
std::size_t writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize);
|
||||||
|
std::size_t writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize);
|
||||||
|
std::size_t writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize);
|
||||||
|
|
||||||
|
struct FileContext
|
||||||
|
{
|
||||||
|
std::filesystem::path filePath;
|
||||||
|
std::size_t fileSize;
|
||||||
|
std::uint32_t fileCrc32;
|
||||||
|
std::size_t localFileHeaderOffset {};
|
||||||
|
};
|
||||||
|
|
||||||
|
using FileContainer = std::map<std::string, FileContext>;
|
||||||
|
FileContainer _files;
|
||||||
|
|
||||||
|
enum class WriteState
|
||||||
|
{
|
||||||
|
LocalFileHeader,
|
||||||
|
LocalFileHeaderFileName,
|
||||||
|
FileData,
|
||||||
|
CentralDirectoryHeader,
|
||||||
|
CentralDirectoryHeaderFileName,
|
||||||
|
EndOfCentralDirectoryRecord,
|
||||||
|
Complete,
|
||||||
|
};
|
||||||
|
|
||||||
|
CompressionMethod _compMethod {CompressionMethod::NoCompression};
|
||||||
|
WriteState _writeState {WriteState::LocalFileHeader};
|
||||||
|
FileContainer::iterator _currentFile;
|
||||||
|
std::size_t _currentOffset {};
|
||||||
|
std::size_t _currentZipOffset {};
|
||||||
|
std::size_t _centralDirectoryOffset {};
|
||||||
|
std::size_t _centralDirectorySize {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Zip
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ add_executable(lms
|
|||||||
ui/explore/TracksView.cpp
|
ui/explore/TracksView.cpp
|
||||||
ui/resource/AudioFileResource.cpp
|
ui/resource/AudioFileResource.cpp
|
||||||
ui/resource/AudioTranscodeResource.cpp
|
ui/resource/AudioTranscodeResource.cpp
|
||||||
|
ui/resource/DownloadResource.cpp
|
||||||
ui/resource/ImageResource.cpp
|
ui/resource/ImageResource.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
#include "admin/UsersView.hpp"
|
#include "admin/UsersView.hpp"
|
||||||
#include "resource/AudioFileResource.hpp"
|
#include "resource/AudioFileResource.hpp"
|
||||||
#include "resource/AudioTranscodeResource.hpp"
|
#include "resource/AudioTranscodeResource.hpp"
|
||||||
|
#include "resource/DownloadResource.hpp"
|
||||||
#include "resource/ImageResource.hpp"
|
#include "resource/ImageResource.hpp"
|
||||||
#include "Auth.hpp"
|
#include "Auth.hpp"
|
||||||
#include "LmsApplicationException.hpp"
|
#include "LmsApplicationException.hpp"
|
||||||
@@ -429,9 +430,9 @@ LmsApplication::handleUserLoggedIn(Database::IdType userId, bool strongAuth)
|
|||||||
void
|
void
|
||||||
LmsApplication::createHome()
|
LmsApplication::createHome()
|
||||||
{
|
{
|
||||||
_imageResource = std::make_shared<ImageResource>();
|
|
||||||
_audioTranscodeResource = std::make_shared<AudioTranscodeResource>();
|
|
||||||
_audioFileResource = std::make_shared<AudioFileResource>();
|
_audioFileResource = std::make_shared<AudioFileResource>();
|
||||||
|
_audioTranscodeResource = std::make_shared<AudioTranscodeResource>();
|
||||||
|
_imageResource = std::make_shared<ImageResource>();
|
||||||
|
|
||||||
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
||||||
doJavaScript("$('body').tooltip({ selector: '[data-toggle=\"tooltip\"]'})");
|
doJavaScript("$('body').tooltip({ selector: '[data-toggle=\"tooltip\"]'})");
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "resource/ImageResource.hpp"
|
#include "resource/DownloadResource.hpp"
|
||||||
#include "ArtistListHelpers.hpp"
|
#include "ArtistListHelpers.hpp"
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
@@ -131,6 +131,8 @@ Artist::refreshView()
|
|||||||
{
|
{
|
||||||
artistsAction.emit(PlayQueueAction::PlayLast, {*artistId});
|
artistsAction.emit(PlayQueueAction::PlayLast, {*artistId});
|
||||||
});
|
});
|
||||||
|
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
|
||||||
|
->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(*artistId)});
|
||||||
|
|
||||||
popup->exec(moreBtn);
|
popup->exec(moreBtn);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "ReleasePopup.hpp"
|
#include "ReleasePopup.hpp"
|
||||||
|
|
||||||
|
#include "resource/DownloadResource.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
@@ -41,6 +42,8 @@ namespace UserInterface
|
|||||||
{
|
{
|
||||||
releasesAction.emit(PlayQueueAction::PlayLast, {releaseId});
|
releasesAction.emit(PlayQueueAction::PlayLast, {releaseId});
|
||||||
});
|
});
|
||||||
|
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
|
||||||
|
->setLink(Wt::WLink {std::make_unique<DownloadReleaseResource>(releaseId)});
|
||||||
|
|
||||||
popup->popup(&target);
|
popup->popup(&target);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "DownloadResource.hpp"
|
||||||
|
|
||||||
|
#include <Wt/WApplication.h>
|
||||||
|
#include <Wt/Http/Response.h>
|
||||||
|
|
||||||
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
#include "utils/Exception.hpp"
|
||||||
|
#include "utils/Logger.hpp"
|
||||||
|
#include "utils/Zipper.hpp"
|
||||||
|
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
|
#define LOG(level) LMS_LOG(UI, level) << "Download resource: "
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
|
||||||
|
DownloadResource::~DownloadResource()
|
||||||
|
{
|
||||||
|
beingDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::shared_ptr<Zip::Zipper> zipper;
|
||||||
|
|
||||||
|
// First, see if this request is for a continuation
|
||||||
|
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||||
|
if (continuation)
|
||||||
|
zipper = Wt::cpp17::any_cast<std::shared_ptr<Zip::Zipper>>(continuation->data());
|
||||||
|
else
|
||||||
|
{
|
||||||
|
zipper = createZipper();
|
||||||
|
response.setMimeType("application/zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!zipper)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::array<std::byte, bufferSize> buffer;
|
||||||
|
std::size_t nbWrittenBytes {zipper->writeSome(buffer.data(), buffer.size())};
|
||||||
|
|
||||||
|
response.out().write(reinterpret_cast<const char *>(buffer.data()), nbWrittenBytes);
|
||||||
|
|
||||||
|
if (!zipper->isComplete())
|
||||||
|
{
|
||||||
|
auto* continuation {response.createContinuation()};
|
||||||
|
continuation->setData(zipper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Zip::ZipperException& exception)
|
||||||
|
{
|
||||||
|
LOG(ERROR) << "Zipper exception: " << exception.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static
|
||||||
|
std::string
|
||||||
|
getArtistPathName(Database::Artist::pointer artist)
|
||||||
|
{
|
||||||
|
return StringUtils::replaceInString(artist->getName(), "/", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
std::string
|
||||||
|
getReleaseArtistPathName(Database::Release::pointer release)
|
||||||
|
{
|
||||||
|
std::string releaseArtistName;
|
||||||
|
|
||||||
|
std::vector<Wt::Dbo::ptr<Database::Artist>> artists;
|
||||||
|
|
||||||
|
artists = release->getReleaseArtists();
|
||||||
|
if (artists.empty())
|
||||||
|
artists = release->getArtists();
|
||||||
|
|
||||||
|
if (artists.size() > 1)
|
||||||
|
releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8();
|
||||||
|
else if (artists.size() == 1)
|
||||||
|
releaseArtistName = artists.front()->getName();
|
||||||
|
|
||||||
|
releaseArtistName = StringUtils::replaceInString(releaseArtistName, "/", "_");
|
||||||
|
|
||||||
|
return releaseArtistName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
std::string
|
||||||
|
getReleasePathName(Database::Release::pointer release)
|
||||||
|
{
|
||||||
|
std::string releaseName;
|
||||||
|
|
||||||
|
if (auto releaseYear {release->getReleaseYear()})
|
||||||
|
releaseName += std::to_string(*releaseYear) + " - ";
|
||||||
|
releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
|
||||||
|
|
||||||
|
return releaseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
std::string
|
||||||
|
getTrackPathName(Database::Track::pointer track)
|
||||||
|
{
|
||||||
|
std::string fileName;
|
||||||
|
|
||||||
|
auto trackNumber {track->getTrackNumber()};
|
||||||
|
auto discNumber {track->getDiscNumber()};
|
||||||
|
|
||||||
|
if (discNumber)
|
||||||
|
fileName += std::to_string(*discNumber) + ".";
|
||||||
|
if (trackNumber)
|
||||||
|
fileName += std::to_string(*trackNumber) + " - ";
|
||||||
|
|
||||||
|
fileName += track->getName() + track->getPath().filename().extension().string();
|
||||||
|
fileName = StringUtils::replaceInString(fileName , "/", "_");
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
std::unique_ptr<Zip::Zipper>
|
||||||
|
createZipper(const std::vector<Database::Track::pointer>& tracks)
|
||||||
|
{
|
||||||
|
std::map<std::string, std::filesystem::path> files;
|
||||||
|
|
||||||
|
for (const Database::Track::pointer& track : tracks)
|
||||||
|
{
|
||||||
|
std::string releaseName;
|
||||||
|
std::string releaseArtistName;
|
||||||
|
if (auto release {track->getRelease()})
|
||||||
|
{
|
||||||
|
releaseName = getReleasePathName(release);
|
||||||
|
releaseArtistName = getReleaseArtistPathName(release);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fileName;
|
||||||
|
if (!releaseArtistName.empty())
|
||||||
|
fileName += releaseArtistName + "/";
|
||||||
|
if (!releaseName.empty())
|
||||||
|
fileName += releaseName + "/";
|
||||||
|
fileName += getTrackPathName(track);
|
||||||
|
|
||||||
|
files.emplace(fileName, track->getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_unique<Zip::Zipper>(files, Zip::Zipper::CompressionMethod::NoCompression);
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadReleaseResource::DownloadReleaseResource(Database::IdType releaseId)
|
||||||
|
: _releaseId {releaseId}
|
||||||
|
{
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)};
|
||||||
|
if (release)
|
||||||
|
suggestFileName(getReleasePathName(release) + ".zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::unique_ptr<Zip::Zipper>
|
||||||
|
DownloadReleaseResource::createZipper()
|
||||||
|
{
|
||||||
|
Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), _releaseId)};
|
||||||
|
if (!release)
|
||||||
|
{
|
||||||
|
LOG(DEBUG) << "Cannot find release";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserInterface::createZipper(release->getTracks());
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadArtistResource::DownloadArtistResource(Database::IdType artistId)
|
||||||
|
: _artistId {artistId}
|
||||||
|
{
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)};
|
||||||
|
if (artist)
|
||||||
|
suggestFileName(getArtistPathName(artist) + ".zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Zip::Zipper>
|
||||||
|
DownloadArtistResource::createZipper()
|
||||||
|
{
|
||||||
|
Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)};
|
||||||
|
if (!artist)
|
||||||
|
{
|
||||||
|
LOG(DEBUG) << "Cannot find artist";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserInterface::createZipper(artist->getTracks());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <Wt/WResource.h>
|
||||||
|
|
||||||
|
#include "database/Types.hpp"
|
||||||
|
#include "utils/Zipper.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
|
||||||
|
class DownloadResource : public Wt::WResource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr std::size_t bufferSize {32768};
|
||||||
|
|
||||||
|
~DownloadResource();
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||||
|
virtual std::unique_ptr<Zip::Zipper> createZipper() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DownloadReleaseResource : public DownloadResource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DownloadReleaseResource(Database::IdType releaseId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Zip::Zipper> createZipper() override;
|
||||||
|
Database::IdType _releaseId;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DownloadArtistResource : public DownloadResource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DownloadArtistResource(Database::IdType artistId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Zip::Zipper> createZipper() override;
|
||||||
|
Database::IdType _artistId;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
|
|
||||||
@@ -47,7 +47,8 @@ class ImageResource : public Wt::WResource
|
|||||||
|
|
||||||
static std::string getMimeType();
|
static std::string getMimeType();
|
||||||
|
|
||||||
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response);
|
private:
|
||||||
|
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
add_subdirectory(metadata)
|
add_subdirectory(metadata)
|
||||||
add_subdirectory(recommendation)
|
add_subdirectory(recommendation)
|
||||||
|
add_subdirectory(zipper)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
add_executable(lms-zipper
|
||||||
|
LmsZipper.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(lms-zipper PRIVATE
|
||||||
|
lmsutils
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "utils/StreamLogger.hpp"
|
||||||
|
#include "utils/Zipper.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
// log to stdout
|
||||||
|
Service<Logger> logger {std::make_unique<StreamLogger>(std::cout)};
|
||||||
|
|
||||||
|
if (argc < 2)
|
||||||
|
{
|
||||||
|
std::cerr << "Usage: <archive> <file> [...]" << std::endl;
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path zipPath {argv[1]};
|
||||||
|
|
||||||
|
std::map<std::string, std::filesystem::path> files;
|
||||||
|
for (int i {2}; i < argc; ++i)
|
||||||
|
files.emplace(argv[i], argv[i]);
|
||||||
|
|
||||||
|
|
||||||
|
std::cout << "Compressing " << files.size() << " files..." << std::endl;
|
||||||
|
|
||||||
|
using namespace Zip;
|
||||||
|
|
||||||
|
std::ofstream ofs {zipPath.string().c_str(), std::ios_base::binary};
|
||||||
|
if (!ofs)
|
||||||
|
{
|
||||||
|
std::cerr << "Cannot open file '" << zipPath.string() << "' for writing";
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Zipper zipper {files, Zipper::CompressionMethod::NoCompression};
|
||||||
|
|
||||||
|
while (!zipper.isComplete())
|
||||||
|
{
|
||||||
|
std::array<std::byte, 65536> buffer;
|
||||||
|
|
||||||
|
std::cout << "Call" << std::endl;
|
||||||
|
std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())};
|
||||||
|
std::cout << "nbWrittenBytes = " << nbWrittenBytes << std::endl;
|
||||||
|
|
||||||
|
ofs.write(reinterpret_cast<const char*>(buffer.data()), nbWrittenBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user