/* * scribiu -- read notebooks and voice memos from Livescribe pens * Copyright (C) 2015 Javier S. Pedro * * This program 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. * * This program 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 this program. If not, see . */ #include #include #include #include #include #include #include "paperreplay.h" #include "notebookmodel.h" #include "smartpensyncer.h" #define BUFFER_SIZE 16 * 1024 namespace { class LockFile { public: LockFile(const QString &path); ~LockFile(); bool lock(); private: QString _path; FILE * _file; bool _locked; }; class TimestampFile { public: TimestampFile(const QString &path); QDateTime get(); void set(); private: QFileInfo _fi; }; LockFile::LockFile(const QString &path) : _path(path), _file(0), _locked(false) { } LockFile::~LockFile() { if (_file) { fclose(_file); } if (_locked) { if (!QFile::remove(_path)) { qWarning() << "Cannot remove lock file:" << _path; } } } bool LockFile::lock() { Q_ASSERT(!_locked); QFileInfo info(_path); if (info.exists()) { if (info.created().secsTo(QDateTime::currentDateTime()) > 10 * 60) { if (QFile::remove(info.filePath())) { qDebug() << "Removing stale lock file:" << info.absoluteFilePath(); } } else { return false; } } _file = ::fopen(info.absoluteFilePath().toLocal8Bit().data(), "wx"); if (_file) { _locked = true; return true; } else { return false; } } TimestampFile::TimestampFile(const QString &path) : _fi(path) { } QDateTime TimestampFile::get() { qDebug() << "Checking timestamp" << _fi.filePath(); if (_fi.exists()) { return _fi.lastModified(); } else { return QDateTime(); } } void TimestampFile::set() { QFile f(_fi.filePath()); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { qWarning() << "Could not set timestamp file:" << _fi.absoluteFilePath(); return; } f.close(); _fi.refresh(); } } SmartpenSyncer::SmartpenSyncer(const Smartpen::Address &addr, QObject *parent) : QThread(parent), _addr(addr), _pen(new Smartpen(this)), _errored(false), _aborted(false) { } SmartpenSyncer::~SmartpenSyncer() { if (isRunning()) { _aborted = true; wait(); } } Smartpen::Address SmartpenSyncer::penAddress() const { return _addr; } QString SmartpenSyncer::penName() const { return _penName; } bool SmartpenSyncer::hasErrors() const { return _errored; } void SmartpenSyncer::abort() { _aborted = true; } void SmartpenSyncer::run() { if (!_pen->connectToPen(_addr)) { qWarning() << "Could not connect to pen with USB address: " << _addr; _errored = true; return; } _penName = _pen->getPenName(); qDebug() << "got pen name:" << _penName; emit penNameChanged(); QVariantMap penInfo = _pen->getPenInfo(); if (penInfo.isEmpty()) { qWarning() << "Could not get pen info"; _errored = true; return; } _penSerial = penInfo["penserial"].toString(); _penDataDir.setPath(NotebookModel::userDataDirectory() + "/" + _penName + ".pen"); if (!_penDataDir.exists()) { if (!_penDataDir.mkpath(".")) { qWarning() << "Cannot create pen data directory:" << _penDataDir.absolutePath(); } } if (!syncPen()) { _errored = true; } _pen->disconnectFromPen(); } bool SmartpenSyncer::syncPen() { LockFile lock(_penDataDir.filePath(".sync.lck")); if (!lock.lock()) { qWarning() << "Pen is already being synchronized; delete this file if it is not:" << _penDataDir.absoluteFilePath(".sync.lck"); return false; } TimestampFile lastSync(_penDataDir.filePath(".lastsync")); QList changes = _pen->getChangeList(lastSync.get()); foreach(const Smartpen::ChangeReport &change, changes) { if (!change.guid.isEmpty()) { qDebug() << "Synchronizing guid: " << change.guid << change.title; if (!syncNotebook(change)) { return false; } } else if (change.className == "com.livescribe.paperreplay.PaperReplay") { qDebug() << "Synchronizing paper replay"; if (!syncPaperReplay()) { return false; } } if (_aborted) { qWarning() << "Aborting sync"; return false; } } lastSync.set(); return true; } bool SmartpenSyncer::syncNotebook(const Smartpen::ChangeReport &change) { QDir notebookDir(_penDataDir.filePath(change.title + ".afd")); if (!notebookDir.exists()) { if (!notebookDir.mkpath(".")) { qWarning() << "Cannot create notebook data directory:" << notebookDir.absolutePath(); } } LockFile lock(notebookDir.filePath(".sync.lck")); if (!lock.lock()) { qWarning() << "Notebook is already being synchronized; delete this file if it is not:" << notebookDir.absoluteFilePath(".sync.lck"); return false; } TimestampFile lastSync(notebookDir.filePath(".lastsync")); QByteArray lspData = _pen->getLspData(change.guid, lastSync.get()); if (!extractZip(lspData, notebookDir)) { return false; } lastSync.set(); return true; } bool SmartpenSyncer::syncPaperReplay() { QDir replayDir(_penDataDir.filePath(PAPER_REPLAY)); if (!replayDir.exists()) { if (!replayDir.mkpath(".")) { qWarning() << "Cannot create PaperReplay data directory:" << replayDir.absolutePath(); } } LockFile lock(replayDir.filePath(".sync.lck")); if (!lock.lock()) { qWarning() << "Paper replay is already being synchronized; delete this file if it is not:" << replayDir.absoluteFilePath(".sync.lck"); return false; } TimestampFile lastSync(replayDir.filePath(".lastsync")); QByteArray replayData = _pen->getPaperReplay(lastSync.get()); if (!extractZip(replayData, replayDir)) { return false; } lastSync.set(); return true; } bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir) { QBuffer zipBuffer(&zipData); QuaZip zip(&zipBuffer); QuaZipFile zipFile(&zip); if (!zip.open(QuaZip::mdUnzip)) { qWarning() << "Could not open zip file"; return false; } QScopedArrayPointer buffer(new char[BUFFER_SIZE]); for (bool more=zip.goToFirstFile(); more; more=zip.goToNextFile()) { QString zipName = zip.getCurrentFileName(); if (!dir.absoluteFilePath(zipName).startsWith(dir.absolutePath())) { qWarning() << "broken zip filename:" << zipName; continue; } QFileInfo finfo(dir.filePath(zipName)); if (!dir.mkpath(finfo.path())) { qWarning() << "cannot mkpath for:" << finfo.absoluteFilePath(); } if (zipName.endsWith('/')) { // Nothing to do continue; } if (!zipFile.open(QIODevice::ReadOnly)) { qWarning() << "cannot open zip file for reading:" << zipName; continue; } QFile file(finfo.filePath()); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { qWarning() << "cannot open for writing:" << finfo.absoluteFilePath(); zipFile.close(); continue; } while (!zipFile.atEnd()) { qint64 read = zipFile.read(buffer.data(), BUFFER_SIZE); if (read <= 0) { qWarning() << "short read on:" << zipName; zipFile.close(); continue; } qint64 written = file.write(buffer.data(), read); if (written != read) { qWarning() << "short write on:" << file.fileName(); zipFile.close(); continue; } } file.close(); zipFile.close(); } buffer.reset(); if (zip.getZipError() == UNZ_OK) { return true; } else { qWarning() << "Error while decompressing"; return false; } }