/* * 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 128 * 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); Smartpen::PenTime get(); void set(Smartpen::PenTime time); private: QFile _f; }; 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.lastModified().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) : _f(path) { } Smartpen::PenTime TimestampFile::get() { if (_f.exists()) { if (_f.open(QIODevice::ReadOnly | QIODevice::Text)) { QString data = QString::fromUtf8(_f.readLine(32)); _f.close(); return data.toULongLong(); } else { qWarning() << "Could not read timestamp file:" << _f.fileName(); } } return 0; } void TimestampFile::set(Smartpen::PenTime time) { if (_f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { _f.write(QString::number(time).toUtf8()); _f.close(); } else { qWarning() << "Could not set timestamp file:" << _f.fileName(); return; } } } /* anonymous namespace */ SmartpenSyncer::SmartpenSyncer(const Smartpen::Address &addr, QObject *parent) : QThread(parent), _addr(addr), _errored(false), _aborted(false), _pen(new Smartpen(this)) { connect(_pen, SIGNAL(linkError(QString)), SLOT(handleLinkError(QString))); } SmartpenSyncer::~SmartpenSyncer() { if (isRunning()) { _aborted = true; wait(); } } Smartpen::Address SmartpenSyncer::penAddress() const { return _addr; } bool SmartpenSyncer::hasErrors() const { return _errored; } void SmartpenSyncer::abort() { _aborted = true; } void SmartpenSyncer::reset() { _aborted = false; _errored = false; } void SmartpenSyncer::run() { if (!_pen->connectToPen(_addr)) { qWarning() << "Could not connect to pen with USB address: " << _addr; _errored = true; return; } if (_aborted) { _pen->disconnectFromPen(); return; } _penName = _pen->getPenName(); qDebug() << "got pen name:" << _penName; if (_penName.isEmpty()) { _penName = _pen->getPenSerial(); qDebug() << "pen with no name, using pen serial instead:" << _penName; } emit gotPenName(_penName); QVariantMap penInfo = _pen->getPenInfo(); if (penInfo.isEmpty()) { qWarning() << "Could not get pen info"; _errored = true; _pen->disconnectFromPen(); return; } _penDataDir.setPath(NotebookModel::userDataDirectory() + "/" + _penName + ".pen"); if (!_penDataDir.exists()) { if (!_penDataDir.mkpath(".")) { qWarning() << "Cannot create pen data directory:" << _penDataDir.absolutePath(); } } if (!syncPen()) { _errored = true; } if (_errored) { qDebug() << "Sync finished with errors"; } else { qDebug() << "Sync finished"; } _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; } // Get the current user time offset from the pen // and store it so that we have it even when the pen is offline TimestampFile userTimeFile(_penDataDir.filePath(PEN_USER_TIME_FILE)); Smartpen::PenTime userTime = _pen->getPenTime(Smartpen::Parameter::UserTime); userTimeFile.set(userTime); qDebug() << "pen time base:" << userTime << Smartpen::fromPenTime(userTime, 0); Smartpen::PenTime penTime = _pen->getPenTime(Smartpen::Parameter::RtcTime); qDebug() << "pen current time:" << penTime << Smartpen::fromPenTime(userTime, penTime); // Read when is the last time we synchronized with this pen (in PenTime, not user time) TimestampFile lastSyncFile(_penDataDir.filePath(PEN_LAST_SYNC_FILE)); Smartpen::PenTime lastSync = lastSyncFile.get(); if (lastSync != 0) lastSync += 1; // We want the changes _from_ the last sync // Ask the pen for all the changes which happened since the last sync time QList changes = _pen->getChangeList(lastSync); Smartpen::PenTime changesEndTime = 0; foreach(const Smartpen::ChangeReport &change, changes) { if (!change.guid.isEmpty()) { qDebug() << "Synchronizing guid: " << change.guid << change.title; if (!syncNotebook(lastSync, change)) { return false; } } else if (change.className == "com.livescribe.paperreplay.PaperReplay") { qDebug() << "Synchronizing paper replay"; if (!syncPaperReplay(lastSync, change)) { return false; } } else { qWarning() << "Unknown change report"; return false; } if (_aborted) { qWarning() << "Aborting sync"; return false; } if (change.endTime > changesEndTime) { changesEndTime = change.endTime; } } if (changesEndTime > 0) { lastSyncFile.set(changesEndTime); } return true; } bool SmartpenSyncer::syncNotebook(Smartpen::PenTime lastSync, const Smartpen::ChangeReport &change) { QDir notebookDir(_penDataDir.filePath(change.title + "." AFD_NOTEBOOK_EXTENSION)); if (!notebookDir.exists()) { if (!notebookDir.mkpath(".")) { qWarning() << "Cannot create notebook data directory:" << notebookDir.absolutePath(); } } LockFile lock(notebookDir.filePath(PEN_SYNC_LOCK_FILE)); if (!lock.lock()) { qWarning() << "Notebook is already being synchronized; delete this file if it is not:" << notebookDir.absoluteFilePath(PEN_SYNC_LOCK_FILE); return false; } QByteArray lspData = _pen->getLspData(change.guid, lastSync); if (!extractZip(lspData, notebookDir)) { return false; } return true; } bool SmartpenSyncer::syncPaperReplay(Smartpen::PenTime lastSync, const Smartpen::ChangeReport &) { QDir replayDir(_penDataDir.filePath(PAPER_REPLAY)); if (!replayDir.exists()) { if (!replayDir.mkpath(".")) { qWarning() << "Cannot create PaperReplay data directory:" << replayDir.absolutePath(); } } LockFile lock(replayDir.filePath(PEN_SYNC_LOCK_FILE)); if (!lock.lock()) { qWarning() << "Paper replay is already being synchronized; delete this file if it is not:" << replayDir.absoluteFilePath(PEN_SYNC_LOCK_FILE); return false; } QByteArray replayData = _pen->getPaperReplay(lastSync); if (!extractZip(replayData, replayDir)) { return false; } 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; } QByteArray buffer(BUFFER_SIZE, Qt::Uninitialized); 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(); } if (zip.getZipError() == UNZ_OK) { return true; } else { qWarning() << "Error while decompressing"; return false; } } void SmartpenSyncer::handleLinkError(const QString &msg) { qWarning() << "link error:" << msg; _errored = true; }