2018-09-16 05:28:46 +02:00
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <qmath.h>
#include <QDataStream>
#include <QDateTime>
#include <QCryptographicHash>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QDirIterator>
#include <QStringBuilder>
#include <QUuid>
2018-09-18 22:07:27 +02:00
#include <QtGlobal>
2018-09-16 05:28:46 +02:00
bool writeBitmap ( const QString & filename , const QByteArray & content )
{
qDebug () << "writeBitmap" << filename ;
QFile file ( filename );
if ( ! file . open ( QIODevice :: WriteOnly | QIODevice :: Truncate ))
{
qWarning () << "could not open file" << file . errorString ();
return false ;
}
2018-10-05 11:50:56 +02:00
const quint64 pixels = qCeil ( content . length () / 4.0 );
const quint32 width = qSqrt ( pixels );
const quint32 height = qCeil ( pixels / ( qreal ) width );
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
const quint32 bitmapSize = width * height * 4 ;
2018-09-16 05:28:46 +02:00
{
QDataStream dataStream ( & file );
dataStream . setByteOrder ( QDataStream :: LittleEndian );
//BMP Header
dataStream << ( quint16 ) 0x4D42 ; //BM-Header
dataStream << ( quint32 )( 54 + bitmapSize ); //File size
dataStream << content . length (); //Unused (and abused for content length)
dataStream << ( quint32 ) 54 ; //Offset to bitmap data
//DIB Header
dataStream << ( quint32 ) 40 ; //DIP Header size
dataStream << width ; //width
dataStream << height ; //height
dataStream << ( quint16 ) 1 ; //Number of color planes
dataStream << ( quint16 ) 32 ; //Bits per pixel
dataStream << ( quint32 ) 0 ; //No compression;
dataStream << bitmapSize ; //Size of bitmap data
dataStream << ( quint32 ) 2835 ; //Horizontal print resolution
dataStream << ( quint32 ) 2835 ; //Horizontal print resolution
dataStream << ( quint32 ) 0 ; //Number of colors in palette
dataStream << ( quint32 ) 0 ; //Important colors
}
file . write ( content );
2018-10-05 11:50:56 +02:00
file . write ( QByteArray (( width * height * 4 ) - content . length (), '\0' ));
2018-09-16 05:28:46 +02:00
return true ;
}
bool readBitmap ( const QString & filename , QByteArray & content )
{
qDebug () << "readBitmap" << filename ;
QFile file ( filename );
if ( ! file . exists ())
{
qWarning () << "file does not exist" ;
return false ;
}
if ( ! file . open ( QIODevice :: ReadOnly ))
{
qWarning () << "could not open file" << file . errorString ();
return false ;
}
if ( file . size () < 14 )
{
qWarning () << "not enough bytes" ;
return false ;
}
QDataStream dataStream ( & file );
dataStream . setByteOrder ( QDataStream :: LittleEndian );
{
quint16 bmHeader ;
dataStream >> bmHeader ;
if ( bmHeader != 0x4D42 )
{
qWarning () << "no BM header" ;
return false ;
}
}
{
quint32 filesize ;
dataStream >> filesize ;
if ( filesize != file . size ())
{
qWarning () << "file size does not match!" ;
return false ;
}
}
quint32 usedSize ;
dataStream >> usedSize ;
quint32 offsetBitmapData ;
dataStream >> offsetBitmapData ;
if ( ! file . seek ( offsetBitmapData ))
{
qWarning () << "could not seek" ;
return false ;
}
content = file . read ( usedSize );
if ( content . length () != usedSize )
{
qWarning () << "could not read enough for usedSize" ;
return false ;
}
return true ;
}
bool emptyDirectory ( const QString & path )
{
QDir dir ( path );
if ( ! dir . exists ())
{
qWarning () << "tried to empty non-existant dir" << path ;
return true ;
}
bool success = true ;
// not empty -- we must empty it first
QDirIterator di ( dir . absolutePath (), QDir :: AllEntries | QDir :: Hidden | QDir :: System | QDir :: NoDotAndDotDot );
while ( di . hasNext ()) {
di . next ();
const QFileInfo & fi = di . fileInfo ();
const QString & filePath = di . filePath ();
bool ok ;
if ( fi . isDir () && ! fi . isSymLink ()) {
ok = QDir ( filePath ). removeRecursively (); // recursive
} else {
ok = QFile :: remove ( filePath );
if ( ! ok ) { // Read-only files prevent directory deletion on Windows, retry with Write permission.
const QFile :: Permissions permissions = QFile :: permissions ( filePath );
if ( ! ( permissions & QFile :: WriteUser ))
ok = QFile :: setPermissions ( filePath , permissions | QFile :: WriteUser )
&& QFile :: remove ( filePath );
}
}
if ( ! ok )
success = false ;
}
if ( ! success )
qWarning () << "could not empty" << path ;
return success ;
}
bool spread ( const QString & sourcePath , const QString & targetPath )
{
qDebug () << "spread" << sourcePath << targetPath ;
2018-10-05 11:50:56 +02:00
const QDir targetDir ( targetPath );
2018-09-16 05:28:46 +02:00
if ( ! targetDir . mkpath ( targetDir . absolutePath ()))
{
qWarning () << "could not create target dir" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const QFileInfo sourceFileInfo ( sourcePath );
2018-09-16 05:28:46 +02:00
if ( ! sourceFileInfo . exists ())
{
qWarning () << "source does not exist" ;
return false ;
}
if ( sourceFileInfo . isFile ())
{
bool rewriteIndex = false ;
2018-10-05 11:50:56 +02:00
if ( QFile :: exists ( targetDir . absoluteFilePath ( QStringLiteral ( "__index.bmp" ))))
2018-09-16 05:28:46 +02:00
{
QByteArray content ;
2018-10-05 11:50:56 +02:00
if ( readBitmap ( targetDir . absoluteFilePath ( QStringLiteral ( "__index.bmp" )), content ))
2018-09-16 05:28:46 +02:00
{
QJsonParseError error ;
auto document = QJsonDocument :: fromJson ( content , & error );
if ( error . error != QJsonParseError :: NoError )
{
qWarning () << "index is invalid: error parsing json" << error . errorString ();
return false ;
}
if ( ! document . isObject ())
{
qWarning () << "index is invalid: json is not an object" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto jsonObject = document . object ();
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
if ( ! jsonObject . contains ( QStringLiteral ( "type" )))
2018-09-16 05:28:46 +02:00
{
qWarning () << "index is invalid: json does not contain type" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto typeValue = jsonObject . value ( QStringLiteral ( "type" ));
2018-09-16 05:28:46 +02:00
if ( typeValue . type () != QJsonValue :: String )
{
qWarning () << "index is invalid: json type is not a string" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto type = typeValue . toString ();
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
if ( type == QStringLiteral ( "file" ))
2018-09-16 05:28:46 +02:00
{
//TODO: compare lastmodified and size
}
2018-10-05 11:50:56 +02:00
else if ( type == QStringLiteral ( "directory" ))
2018-09-16 05:28:46 +02:00
{
qInfo () << "type changed from file to directory" ;
if ( ! emptyDirectory ( targetDir . absolutePath ()))
return false ;
rewriteIndex = true ;
}
else
{
qWarning () << "index is invalid: unknown type" << type ;
rewriteIndex = true ;
}
}
else
rewriteIndex = true ;
}
else
rewriteIndex = true ;
if ( rewriteIndex )
{
if ( ! emptyDirectory ( targetPath ))
return false ;
QFile sourceFile ( sourcePath );
if ( ! sourceFile . open ( QIODevice :: ReadOnly ))
{
qWarning () << "could not open source file" << sourceFile . errorString ();
return false ;
}
QJsonArray parts ;
QCryptographicHash hash ( QCryptographicHash :: Sha512 );
while ( sourceFile . pos () < sourceFile . size ())
{
QString filename ;
QString completePath ;
do
{
2018-10-05 11:50:56 +02:00
filename = QUuid :: createUuid (). toString (). remove ( QLatin1Char ( '{' )). remove ( QLatin1Char ( '}' )) % ".bmp" ;
2018-09-16 05:28:46 +02:00
completePath = targetDir . absoluteFilePath ( filename );
}
while ( QFileInfo ( completePath ). exists ());
QJsonObject part ;
2018-10-05 11:50:56 +02:00
part [ QStringLiteral ( "filename" )] = filename ;
part [ QStringLiteral ( "startPos" )] = sourceFile . pos ();
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
const auto buffer = sourceFile . read ( 2048 * 2048 * 4 );
2018-09-16 05:28:46 +02:00
hash . addData ( buffer );
2018-10-05 11:50:56 +02:00
part [ QStringLiteral ( "endPos" )] = sourceFile . pos ();
part [ QStringLiteral ( "length" )] = buffer . length ();
2018-09-16 05:28:46 +02:00
if ( ! writeBitmap ( completePath , buffer ))
return false ;
parts . append ( part );
}
QJsonObject jsonObject ;
2018-10-05 11:50:56 +02:00
jsonObject [ QStringLiteral ( "type" )] = QStringLiteral ( "file" );
jsonObject [ QStringLiteral ( "filesize" )] = sourceFileInfo . size ();
2018-09-18 22:07:27 +02:00
2018-10-05 11:50:56 +02:00
jsonObject [ QStringLiteral ( "birthTime" )] = sourceFileInfo
2018-09-22 07:28:48 +02:00
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
//deprecated since 5.10
2018-09-18 22:07:27 +02:00
. created ()
2018-09-22 07:28:48 +02:00
#else
. birthTime ()
2018-09-18 22:07:27 +02:00
#endif
. toMSecsSinceEpoch ();
2018-10-05 11:50:56 +02:00
jsonObject [ QStringLiteral ( "lastModified" )] = sourceFileInfo . lastModified (). toMSecsSinceEpoch ();
jsonObject [ QStringLiteral ( "lastRead" )] = sourceFileInfo . lastRead (). toMSecsSinceEpoch ();
jsonObject [ QStringLiteral ( "sha512" )] = QString ( hash . result (). toHex ());
jsonObject [ QStringLiteral ( "parts" )] = parts ;
if ( ! writeBitmap ( targetDir . absoluteFilePath ( QStringLiteral ( "__index.bmp" )),
QJsonDocument ( jsonObject ). toJson ( /* QJsonDocument::Compact */ ))) //amazon has enough storage for spaces!
2018-09-16 05:28:46 +02:00
return false ;
}
}
else if ( sourceFileInfo . isDir ())
{
2018-10-05 11:50:56 +02:00
const QDir sourceDir ( sourcePath );
2018-09-16 05:28:46 +02:00
if ( ! sourceDir . exists ())
{
qWarning () << "source dir does not exist" ;
return false ;
}
bool rewriteIndex = false ;
QStringList oldEntries ;
2018-10-05 11:50:56 +02:00
if ( QFile :: exists ( targetDir . absoluteFilePath ( QStringLiteral ( "__index.bmp" ))))
2018-09-16 05:28:46 +02:00
{
QByteArray content ;
2018-10-05 11:50:56 +02:00
if ( readBitmap ( targetDir . absoluteFilePath ( QStringLiteral ( "__index.bmp" )), content ))
2018-09-16 05:28:46 +02:00
{
QJsonParseError error ;
2018-10-05 11:50:56 +02:00
const auto document = QJsonDocument :: fromJson ( content , & error );
2018-09-16 05:28:46 +02:00
if ( error . error != QJsonParseError :: NoError )
{
qWarning () << "index is invalid: error parsing json" << error . errorString ();
return false ;
}
if ( ! document . isObject ())
{
qWarning () << "index is invalid: json is not an object" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto jsonObject = document . object ();
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
if ( ! jsonObject . contains ( QStringLiteral ( "type" )))
2018-09-16 05:28:46 +02:00
{
qWarning () << "index is invalid: json does not contain type" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto typeValue = jsonObject . value ( QStringLiteral ( "type" ));
2018-09-16 05:28:46 +02:00
if ( typeValue . type () != QJsonValue :: String )
{
qWarning () << "index is invalid: json type is not a string" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto type = typeValue . toString ();
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
if ( type == QStringLiteral ( "file" ))
2018-09-16 05:28:46 +02:00
{
qInfo () << "type changed from directory to file" ;
if ( ! emptyDirectory ( targetDir . absolutePath ()))
return false ;
rewriteIndex = true ;
}
2018-10-05 11:50:56 +02:00
else if ( type == QStringLiteral ( "directory" ))
2018-09-16 05:28:46 +02:00
{
if ( ! jsonObject . contains ( "entries" ))
{
qWarning () << "index is invalid: json does not contain entries" ;
rewriteIndex = true ;
}
2018-10-05 11:50:56 +02:00
const auto entriesValue = jsonObject . value ( QStringLiteral ( "entries" ));
2018-09-16 05:28:46 +02:00
if ( entriesValue . type () != QJsonValue :: Array )
{
qWarning () << "index is invalid: json entries is not an array" ;
rewriteIndex = true ;
}
2018-10-05 11:50:56 +02:00
const auto entries = entriesValue . toArray ();
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
for ( auto value : entries )
2018-09-16 05:28:46 +02:00
{
if ( value . type () != QJsonValue :: String )
{
qWarning () << "index is invalid: json entry is not a string" ;
rewriteIndex = true ;
break ;
}
oldEntries . append ( value . toString ());
}
}
else
{
qWarning () << "index is invalid: unknown type" << type ;
rewriteIndex = true ;
}
}
else
rewriteIndex = true ;
}
else
rewriteIndex = true ;
for ( const auto & oldEntry : oldEntries )
{
2018-10-05 11:50:56 +02:00
const QFileInfo oldFileInfo ( sourceDir . absoluteFilePath ( oldEntry ));
2018-09-16 05:28:46 +02:00
if ( ! oldFileInfo . exists ())
{
qInfo () << "deleted" << sourceDir . absoluteFilePath ( oldEntry );
if ( ! QDir ( targetDir . absoluteFilePath ( oldEntry )). removeRecursively ())
{
qWarning () << "could not remove dir" << targetDir . absoluteFilePath ( oldEntry );
return false ;
}
rewriteIndex = true ;
}
}
QJsonArray entries ;
2018-10-05 11:50:56 +02:00
for ( const auto & fileInfo : sourceDir . entryInfoList ( QDir :: Files | QDir :: Dirs | QDir :: NoDotAndDotDot ))
2018-09-16 05:28:46 +02:00
{
if ( ! oldEntries . contains ( fileInfo . fileName ()))
{
qInfo () << "added" << fileInfo . absoluteFilePath ();
rewriteIndex = true ;
}
entries . append ( fileInfo . fileName ());
if ( ! spread ( fileInfo . absoluteFilePath (), targetDir . absoluteFilePath ( fileInfo . fileName ())))
return false ;
}
if ( rewriteIndex )
{
QJsonObject jsonObject ;
2018-10-05 11:50:56 +02:00
jsonObject [ QStringLiteral ( "type" )] = QStringLiteral ( "directory" );
jsonObject [ QStringLiteral ( "entries" )] = entries ;
if ( ! writeBitmap ( targetDir . absoluteFilePath ( QStringLiteral ( "__index.bmp" )),
QJsonDocument ( jsonObject ). toJson ( /* QJsonDocument::Compact */ ))) //amazon has enough storage for spaces!
2018-09-16 05:28:46 +02:00
return false ;
}
}
return true ;
}
bool compile ( const QString & sourcePath , const QString & targetPath )
{
qDebug () << "compile" << sourcePath << targetPath ;
QFileInfo sourceFileInfo ( sourcePath );
if ( ! sourceFileInfo . exists ())
{
qWarning () << "source does not exist" ;
return false ;
}
if ( ! sourceFileInfo . isDir ())
{
qWarning () << "source is not a dir" ;
return false ;
}
QDir sourceDir ( sourcePath );
QJsonObject jsonObject ;
{
QByteArray content ;
2018-10-05 11:50:56 +02:00
if ( ! readBitmap ( sourceDir . absoluteFilePath ( QStringLiteral ( "__index.bmp" )), content ))
2018-09-16 05:28:46 +02:00
return false ;
QJsonParseError error ;
auto document = QJsonDocument :: fromJson ( content , & error );
if ( error . error != QJsonParseError :: NoError )
{
qWarning () << "error parsing json" << error . errorString ();
return false ;
}
if ( ! document . isObject ())
{
qWarning () << "json is not an object" ;
return false ;
}
jsonObject = document . object ();
}
2018-10-05 11:50:56 +02:00
if ( ! jsonObject . contains ( QStringLiteral ( "type" )))
2018-09-16 05:28:46 +02:00
{
qWarning () << "json does not contain type" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto typeValue = jsonObject . value ( QStringLiteral ( "type" ));
2018-09-16 05:28:46 +02:00
if ( typeValue . type () != QJsonValue :: String )
{
qWarning () << "json type is not a string" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto type = typeValue . toString ();
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
if ( type == QStringLiteral ( "file" ))
2018-09-16 05:28:46 +02:00
{
QFile targetFile ( targetPath );
if ( ! targetFile . open ( QIODevice :: WriteOnly | QIODevice :: Truncate ))
{
qWarning () << "could not open file" << targetFile . errorString ();
return false ;
}
//TODO
}
2018-10-05 11:50:56 +02:00
else if ( type == QStringLiteral ( "directory" ))
2018-09-16 05:28:46 +02:00
{
QDir targetDir ( targetPath );
if ( ! targetDir . mkpath ( targetDir . absolutePath ()))
{
qWarning () << "could not create dir" ;
return false ;
}
2018-10-05 11:50:56 +02:00
if ( ! jsonObject . contains ( QStringLiteral ( "entries" )))
2018-09-16 05:28:46 +02:00
{
qWarning () << "json does not contain entries" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto entriesValue = jsonObject . value ( QStringLiteral ( "entries" ));
2018-09-16 05:28:46 +02:00
if ( entriesValue . type () != QJsonValue :: Array )
{
qWarning () << "json entries is not an array" ;
return false ;
}
2018-10-05 11:50:56 +02:00
const auto entries = entriesValue . toArray ();
2018-09-16 05:28:46 +02:00
2018-10-05 11:50:56 +02:00
for ( auto entryValue : entries )
2018-09-16 05:28:46 +02:00
{
if ( entryValue . type () != QJsonValue :: String )
{
qWarning () << "json entry is not a string" ;
return false ;
}
auto entry = entryValue . toString ();
if ( ! compile ( sourceDir . absoluteFilePath ( entry ), targetDir . absoluteFilePath ( entry )))
return false ;
}
}
else
{
qWarning () << "unknown type" << type ;
return false ;
}
return true ;
}
int main ( int argc , char * argv [])
{
QCoreApplication app ( argc , argv );
QCoreApplication :: setApplicationName ( "picsync" );
QCoreApplication :: setApplicationVersion ( "1.0" );
qSetMessagePattern ( "%{time dd.MM.yyyy HH:mm:ss.zzz} "
"["
"%{if-debug}DEBUG%{endif}"
"%{if-info}INFO%{endif}"
"%{if-warning}WARN%{endif}"
"%{if-critical}CRIT%{endif}"
"%{if-fatal}FATAL%{endif}"
"] "
"%{function}(): "
"%{message}" );
QCommandLineParser parser ;
parser . setApplicationDescription ( QCoreApplication :: translate ( "main" , "Lets you convert any file into pictures. Mostly used in combination with cloud storage." ));
parser . addHelpOption ();
parser . addVersionOption ();
QCommandLineOption actionOption ( QStringList () << "a" << "action" , QCoreApplication :: translate ( "main" , "Action (spread or compile)" ), QCoreApplication :: translate ( "main" , "action" ));
parser . addOption ( actionOption );
QCommandLineOption sourceOption ( QStringList () << "s" << "source" , QCoreApplication :: translate ( "main" , "Source file or directory" ), QCoreApplication :: translate ( "main" , "some_file" ));
parser . addOption ( sourceOption );
QCommandLineOption targetOption ( QStringList () << "t" << "target" , QCoreApplication :: translate ( "main" , "Target directory" ), QCoreApplication :: translate ( "main" , "some_directory" ));
parser . addOption ( targetOption );
parser . process ( app );
if ( ! parser . isSet ( actionOption ))
{
qCritical () << "no action set" ;
parser . showHelp ();
return - 1 ;
}
enum { ActionSpread , ActionCompile } action ;
2018-10-05 11:50:56 +02:00
if ( parser . value ( actionOption ) == QStringLiteral ( "spread" ))
2018-09-16 05:28:46 +02:00
action = ActionSpread ;
2018-10-05 11:50:56 +02:00
else if ( parser . value ( actionOption ) == QStringLiteral ( "compile" ))
2018-09-16 05:28:46 +02:00
action = ActionCompile ;
else
{
qCritical () << "unknown action" << parser . value ( actionOption );
parser . showHelp ();
return - 2 ;
}
if ( ! parser . isSet ( sourceOption ))
{
qCritical () << "source not set" ;
parser . showHelp ();
return - 3 ;
}
QFileInfo sourceFileInfo ( parser . value ( sourceOption ));
if ( ! sourceFileInfo . exists ())
{
qCritical () << "source" << parser . value ( sourceOption ) << "does not exist" ;
parser . showHelp ();
return - 4 ;
}
if ( ! sourceFileInfo . isFile () && ! sourceFileInfo . isDir ())
{
qCritical () << "source" << parser . value ( sourceOption ) << "isnt file nor dir" ;
parser . showHelp ();
return - 5 ;
}
if ( ! parser . isSet ( targetOption ))
{
qCritical () << "target not set" ;
parser . showHelp ();
return - 6 ;
}
2018-10-05 11:50:56 +02:00
const QFileInfo targetFileInfo ( parser . value ( targetOption ));
2018-09-16 05:28:46 +02:00
if ( targetFileInfo . exists () && ! targetFileInfo . isDir ())
{
qCritical () << "target" << parser . value ( targetOption ) << "exists and is not a dir" ;
parser . showHelp ();
return - 7 ;
}
switch ( action )
{
case ActionSpread :
if ( spread ( sourceFileInfo . absoluteFilePath (), targetFileInfo . absoluteFilePath ()))
return 0 ;
else
return - 8 ;
case ActionCompile :
if ( compile ( sourceFileInfo . absoluteFilePath (), targetFileInfo . absoluteFilePath ()))
return 0 ;
else
return - 8 ;
}
Q_UNREACHABLE ();
}