Archiver.cxx 41.2 KB
Newer Older
1
//**************************************************************************
2
//   Copyright 2006 - 2018 Martin Koller, kollix@aon.at
3 4 5 6 7 8
//
//   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, version 2 of the License
//
//**************************************************************************
Martin Koller's avatar
Martin Koller committed
9 10 11

#include <Archiver.hxx>

12
#include <kio_version.h>
13
#include <KTar>
Martin Koller's avatar
Martin Koller committed
14
#include <KFilterBase>
Martin Koller's avatar
Martin Koller committed
15
#include <kio/job.h>
Martin Koller's avatar
Martin Koller committed
16
#include <kio/jobuidelegate.h>
17 18
#include <KProcess>
#include <KMountPoint>
Martin Koller's avatar
Martin Koller committed
19 20
#include <KLocalizedString>
#include <KMessageBox>
Martin Koller's avatar
Martin Koller committed
21

22 23 24 25
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QCursor>
Martin Koller's avatar
Martin Koller committed
26
#include <QTextStream>
Martin Koller's avatar
Martin Koller committed
27 28 29
#include <QFileDialog>
#include <QTemporaryFile>
#include <QTimer>
30
#include <QElapsedTimer>
Martin Koller's avatar
Martin Koller committed
31 32 33 34 35 36 37 38

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/statvfs.h>

Martin Koller's avatar
Martin Koller committed
39 40 41 42 43 44 45 46 47 48 49
// For INT64_MAX:
// The ISO C99 standard specifies that in C++ implementations these
// macros (stdint.h,inttypes.h) should only be defined if explicitly requested.

// ISO C99: 7.18 Integer types
#ifndef __STDC_LIMIT_MACROS
#define __STDC_LIMIT_MACROS
#endif
#include <stdint.h>


50
#include <iostream>
Martin Koller's avatar
Martin Koller committed
51

Martin Koller's avatar
Martin Koller committed
52 53 54
//--------------------------------------------------------------------------------

QString Archiver::sliceScript;
Martin Koller's avatar
Martin Koller committed
55
Archiver *Archiver::instance;
Martin Koller's avatar
Martin Koller committed
56

Martin Koller's avatar
Martin Koller committed
57
const KIO::filesize_t MAX_SLICE = INT64_MAX; // 64bit max value
58

Martin Koller's avatar
Martin Koller committed
59 60 61 62
//--------------------------------------------------------------------------------

Archiver::Archiver(QWidget *parent)
  : QObject(parent),
Laurent Montel's avatar
Laurent Montel committed
63
    archive(nullptr), totalBytes(0), totalFiles(0), filteredFiles(0), sliceNum(0), mediaNeedsChange(false),
Martin Koller's avatar
Martin Koller committed
64
    fullBackupInterval(1), incrementalBackup(false), forceFullBackup(false),
65
    sliceCapacity(MAX_SLICE), compressionType(KCompressionDevice::None), interactive(parent != nullptr),
Martin Koller's avatar
Martin Koller committed
66
    cancelled(false), runs(false), skippedFiles(false), verbose(false), jobResult(0)
Martin Koller's avatar
Martin Koller committed
67
{
Martin Koller's avatar
Martin Koller committed
68 69
  instance = this;

Martin Koller's avatar
Martin Koller committed
70 71
  maxSliceMBs    = Archiver::UNLIMITED;
  numKeptBackups = Archiver::UNLIMITED;
72

Martin Koller's avatar
Martin Koller committed
73
  setCompressFiles(false);
74 75 76

  if ( !interactive )
  {
Laurent Montel's avatar
Laurent Montel committed
77 78
    connect(this, &Archiver::logging, this, &Archiver::loggingSlot);
    connect(this, &Archiver::warning, this, &Archiver::warningSlot);
79 80 81 82 83 84 85 86 87
  }
}

//--------------------------------------------------------------------------------

void Archiver::setCompressFiles(bool b)
{
  if ( b )
  {
88 89 90
    ext = QStringLiteral(".xz");
    compressionType = KCompressionDevice::Xz;
    KFilterBase *base = KCompressionDevice::filterForCompressionType(compressionType);
Martin Koller's avatar
Martin Koller committed
91
    if ( !base )
92 93 94 95 96 97 98 99 100 101
    {
      ext = QStringLiteral(".bz2");
      compressionType = KCompressionDevice::BZip2;
      base = KCompressionDevice::filterForCompressionType(compressionType);
      if ( !base )
      {
        ext = QStringLiteral(".gz");
        compressionType = KCompressionDevice::GZip;
      }
    }
Martin Koller's avatar
Martin Koller committed
102 103

    delete base;
104 105
  }
  else
Martin Koller's avatar
Martin Koller committed
106
  {
107
    ext = QString();
Martin Koller's avatar
Martin Koller committed
108 109 110 111 112
  }
}

//--------------------------------------------------------------------------------

Martin Koller's avatar
Martin Koller committed
113
void Archiver::setTarget(const QUrl &target)
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
{
  targetURL = target;
  calculateCapacity();
}

//--------------------------------------------------------------------------------

void Archiver::setMaxSliceMBs(int mbs)
{
  maxSliceMBs = mbs;
  calculateCapacity();
}

//--------------------------------------------------------------------------------

Martin Koller's avatar
Martin Koller committed
129 130 131 132 133 134 135
void Archiver::setKeptBackups(int num)
{
  numKeptBackups = num;
}

//--------------------------------------------------------------------------------

Martin Koller's avatar
Martin Koller committed
136 137 138
void Archiver::setFilter(const QString &filter)
{
  filters.clear();
139
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
140
  const QStringList list = filter.split(QLatin1Char(' '), QString::SkipEmptyParts);
141
#else
142
  const QStringList list = filter.split(QLatin1Char(' '), Qt::SkipEmptyParts);
143
#endif
144 145
  filters.reserve(list.count());
  for (const QString &str : list)
Martin Koller's avatar
Martin Koller committed
146 147 148 149 150 151 152 153
    filters.append(QRegExp(str, Qt::CaseSensitive, QRegExp::Wildcard));
}

//--------------------------------------------------------------------------------

QString Archiver::getFilter() const
{
  QString filter;
154
  for (const QRegExp &reg : qAsConst(filters))
Martin Koller's avatar
Martin Koller committed
155 156
  {
    filter += reg.pattern();
157
    filter += QLatin1Char(' ');
Martin Koller's avatar
Martin Koller committed
158 159 160 161 162 163
  }
  return filter;
}

//--------------------------------------------------------------------------------

Martin Koller's avatar
Martin Koller committed
164 165 166
void Archiver::setDirFilter(const QString &filter)
{
  dirFilters.clear();
167
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
168
  const QStringList list = filter.split(QLatin1Char('\n'), QString::SkipEmptyParts);
169
#else
170
  const QStringList list = filter.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
171
#endif
172
  for (const QString &str : list)
Martin Koller's avatar
Martin Koller committed
173 174 175 176 177 178 179 180 181 182 183 184
  {
    QString expr = str.trimmed();
    if ( !expr.isEmpty() )
      dirFilters.append(QRegExp(expr, Qt::CaseSensitive, QRegExp::Wildcard));
  }
}

//--------------------------------------------------------------------------------

QString Archiver::getDirFilter() const
{
  QString filter;
185
  for (const QRegExp &reg : qAsConst(dirFilters))
Martin Koller's avatar
Martin Koller committed
186 187
  {
    filter += reg.pattern();
188
    filter += QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
189 190 191 192 193 194
  }
  return filter;
}

//--------------------------------------------------------------------------------

Martin Koller's avatar
Martin Koller committed
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
void Archiver::setFullBackupInterval(int days)
{
  fullBackupInterval = days;

  if ( fullBackupInterval == 1 )
  {
    setIncrementalBackup(false);
    lastFullBackup = QDateTime();
    lastBackup = QDateTime();
  }
}

//--------------------------------------------------------------------------------

void Archiver::setForceFullBackup(bool force)
{
  forceFullBackup = force;
212
  Q_EMIT backupTypeChanged(isIncrementalBackup());
Martin Koller's avatar
Martin Koller committed
213 214 215 216 217 218 219
}

//--------------------------------------------------------------------------------

void Archiver::setIncrementalBackup(bool inc)
{
  incrementalBackup = inc;
220
  Q_EMIT backupTypeChanged(isIncrementalBackup());
Martin Koller's avatar
Martin Koller committed
221 222 223 224
}

//--------------------------------------------------------------------------------

225 226 227 228 229 230 231 232 233
void Archiver::setFilePrefix(const QString &prefix)
{
  filePrefix = prefix;
}

//--------------------------------------------------------------------------------

void Archiver::calculateCapacity()
{
Martin Koller's avatar
Martin Koller committed
234 235
  if ( targetURL.isEmpty() ) return;

236 237 238
  // calculate how large a slice can actually be
  // - limited by the target directory (when we store directly into a local dir)
  // - limited by the "tmp" dir when we create a tmp file for later upload via KIO
Martin Koller's avatar
Martin Koller committed
239
  // - limited by Qt (64bit int)
240
  // - limited by user defined maxSliceMBs
241

242
  KIO::filesize_t totalBytes = 0;
243

244
  if ( targetURL.isLocalFile() )
Martin Koller's avatar
Martin Koller committed
245 246 247 248
  {
    if ( ! getDiskFree(targetURL.path(), totalBytes, sliceCapacity) )
      return;
  }
249 250
  else
  {
Martin Koller's avatar
Martin Koller committed
251
    getDiskFree(QDir::tempPath() + QLatin1Char('/'), totalBytes, sliceCapacity);
252 253 254 255
    // as "tmp" is also used by others and by us when compressing a file,
    // don't eat it up completely. Reserve 10%
    sliceCapacity = sliceCapacity * 9 / 10;
  }
256

Martin Koller's avatar
Martin Koller committed
257 258
  // limit to what Qt can handle
  sliceCapacity = qMin(sliceCapacity, MAX_SLICE);
259

260 261 262
  if ( maxSliceMBs != UNLIMITED )
  {
    KIO::filesize_t max = static_cast<KIO::filesize_t>(maxSliceMBs) * 1024 * 1024;
Martin Koller's avatar
Martin Koller committed
263
    sliceCapacity = qMin(sliceCapacity, max);
264
  }
265

266
  sliceBytes = 0;
267 268 269 270

  // if the disk is full (capacity == 0), don't tell the user "unlimited"
  // sliceCapacity == 0 has a special meaning as "unlimited"; see MainWidget.cxx
  if ( sliceCapacity == 0 ) sliceCapacity = 1;
271
  Q_EMIT targetCapacity(sliceCapacity);
272
}
273

274
//--------------------------------------------------------------------------------
275

276 277 278
bool Archiver::loadProfile(const QString &fileName, QStringList &includes, QStringList &excludes, QString &error)
{
  QFile file(fileName);
Martin Koller's avatar
Martin Koller committed
279
  if ( ! file.open(QIODevice::ReadOnly) )
280 281 282
  {
    error = file.errorString();
    return false;
283
  }
284

Martin Koller's avatar
Martin Koller committed
285 286
  loadedProfile = fileName;

287 288 289 290 291
  QString target;
  QChar type, blank;
  QTextStream stream(&file);

  // back to default (in case old profile read which does not include these)
292
  setFilePrefix(QString());
Martin Koller's avatar
Martin Koller committed
293
  setMaxSliceMBs(Archiver::UNLIMITED);
Martin Koller's avatar
Martin Koller committed
294
  setFullBackupInterval(1);  // default as in previous versions
Martin Koller's avatar
Martin Koller committed
295
  filters.clear();
Martin Koller's avatar
Martin Koller committed
296
  dirFilters.clear();
297 298

  while ( ! stream.atEnd() )
299
  {
300 301 302 303
    stream.skipWhiteSpace();
    stream >> type;            // read a QChar without skipping whitespace
    stream >> blank;           // read a QChar without skipping whitespace

304
    if ( type == QLatin1Char('M') )
305 306 307
    {
      target = stream.readLine();  // include white space
    }
308
    else if ( type == QLatin1Char('P') )
309 310
    {
      QString prefix = stream.readLine();  // include white space
Martin Koller's avatar
Martin Koller committed
311 312
      setFilePrefix(prefix);
    }
313
    else if ( type == QLatin1Char('R') )
Martin Koller's avatar
Martin Koller committed
314 315 316 317
    {
      int max;
      stream >> max;
      setKeptBackups(max);
318
    }
319
    else if ( type == QLatin1Char('F') )
Martin Koller's avatar
Martin Koller committed
320 321 322 323 324
    {
      int days;
      stream >> days;
      setFullBackupInterval(days);
    }
325
    else if ( type == QLatin1Char('B') )  // last dateTime for backup
Martin Koller's avatar
Martin Koller committed
326 327 328 329 330
    {
      QString dateTime;
      stream >> dateTime;
      lastBackup = QDateTime::fromString(dateTime, Qt::ISODate);
    }
331
    else if ( type == QLatin1Char('L') )  // last dateTime for full backup
Martin Koller's avatar
Martin Koller committed
332 333 334 335 336
    {
      QString dateTime;
      stream >> dateTime;
      lastFullBackup = QDateTime::fromString(dateTime, Qt::ISODate);
    }
337
    else if ( type == QLatin1Char('S') )
338 339 340
    {
      int max;
      stream >> max;
Martin Koller's avatar
Martin Koller committed
341
      setMaxSliceMBs(max);
342
    }
343
    else if ( type == QLatin1Char('C') )
344 345 346
    {
      int change;
      stream >> change;
Martin Koller's avatar
Martin Koller committed
347
      setMediaNeedsChange(change);
348
    }
349
    else if ( type == QLatin1Char('X') )
Martin Koller's avatar
Martin Koller committed
350 351 352
    {
      setFilter(stream.readLine());  // include white space
    }
353
    else if ( type == QLatin1Char('x') )
Martin Koller's avatar
Martin Koller committed
354 355 356
    {
      dirFilters.append(QRegExp(stream.readLine(), Qt::CaseSensitive, QRegExp::Wildcard));
    }
357
    else if ( type == QLatin1Char('Z') )
358 359 360
    {
      int compress;
      stream >> compress;
Martin Koller's avatar
Martin Koller committed
361
      setCompressFiles(compress);
362
    }
363
    else if ( type == QLatin1Char('I') )
364 365 366
    {
      includes.append(stream.readLine());
    }
367
    else if ( type == QLatin1Char('E') )
368 369 370
    {
      excludes.append(stream.readLine());
    }
Martin Koller's avatar
Martin Koller committed
371 372
    else
      stream.readLine();  // skip unknown key and rest of line
373
  }
374 375 376

  file.close();

Martin Koller's avatar
Martin Koller committed
377
  setTarget(QUrl::fromUserInput(target));
378

Martin Koller's avatar
Martin Koller committed
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
  setIncrementalBackup(
    (fullBackupInterval > 1) && lastFullBackup.isValid() &&
    (lastFullBackup.daysTo(QDateTime::currentDateTime()) < fullBackupInterval));

  return true;
}

//--------------------------------------------------------------------------------

bool Archiver::saveProfile(const QString &fileName, const QStringList &includes, const QStringList &excludes, QString &error)
{
  QFile file(fileName);

  if ( ! file.open(QIODevice::WriteOnly) )
  {
    error = file.errorString();
    return false;
  }

  QTextStream stream(&file);

400 401 402 403 404
  stream << "M " << targetURL.toString(QUrl::PreferLocalFile) << QLatin1Char('\n');
  stream << "P " << getFilePrefix() << QLatin1Char('\n');
  stream << "S " << getMaxSliceMBs() << QLatin1Char('\n');
  stream << "R " << getKeptBackups() << QLatin1Char('\n');
  stream << "F " << getFullBackupInterval() << QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
405 406

  if ( getLastFullBackup().isValid() )
407
    stream << "L " << getLastFullBackup().toString(Qt::ISODate) << QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
408 409

  if ( getLastBackup().isValid() )
410
    stream << "B " << getLastBackup().toString(Qt::ISODate) << QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
411

412 413
  stream << "C " << static_cast<int>(getMediaNeedsChange()) << QLatin1Char('\n');
  stream << "Z " << static_cast<int>(getCompressFiles()) << QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
414

Martin Koller's avatar
Martin Koller committed
415
  if ( !filters.isEmpty() )
416
    stream << "X " << getFilter() << QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
417

418
  for (const QRegExp &exp : qAsConst(dirFilters))
419
    stream << "x " << exp.pattern() << QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
420

421
  for (const QString &str : includes)
422
    stream << "I " << str << QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
423

424
  for (const QString &str : excludes)
425
    stream << "E " << str << QLatin1Char('\n');
Martin Koller's avatar
Martin Koller committed
426 427

  file.close();
428
  return true;
429 430 431 432
}

//--------------------------------------------------------------------------------

433
bool Archiver::createArchive(const QStringList &includes, const QStringList &excludes)
Martin Koller's avatar
Martin Koller committed
434
{
435
  if ( includes.isEmpty() )
Martin Koller's avatar
Martin Koller committed
436
  {
437
    Q_EMIT warning(i18n("Nothing selected for backup"));
438
    return false;
Martin Koller's avatar
Martin Koller committed
439 440
  }

441 442
  if ( !targetURL.isValid() )
  {
443
    Q_EMIT warning(i18n("The target dir '%1' is not valid", targetURL.toString()));
444 445 446
    return false;
  }

447
  // non-interactive mode only allows local targets as KIO needs $DISPLAY
448
  if ( !interactive && !targetURL.isLocalFile() )
Martin Koller's avatar
Martin Koller committed
449
  {
450
    Q_EMIT warning(i18n("The target dir '%1' must be a local file system dir and no remote URL",
451
                     targetURL.toString()));
452
    return false;
Martin Koller's avatar
Martin Koller committed
453 454
  }

Martin Koller's avatar
Martin Koller committed
455 456 457 458 459 460 461 462 463
  // check if the target dir exists and optionally create it
  if ( targetURL.isLocalFile() )
  {
    QDir dir(targetURL.path());
    if ( !dir.exists() )
    {
      if ( !interactive ||
           (KMessageBox::warningYesNo(static_cast<QWidget*>(parent()),
              i18n("The target directory '%1' does not exist.\n\n"
464
                   "Shall I create it?", dir.absolutePath())) == KMessageBox::Yes) )
Martin Koller's avatar
Martin Koller committed
465
      {
466
        if ( !dir.mkpath(QStringLiteral(".")) )
Martin Koller's avatar
Martin Koller committed
467
        {
468
          Q_EMIT warning(i18n("Could not create the target directory '%1'.\n"
469
                            "The operating system reports: %2", dir.absolutePath(), QString::fromLatin1(strerror(errno))));
Martin Koller's avatar
Martin Koller committed
470 471 472 473 474
          return false;
        }
      }
      else
      {
475
        Q_EMIT warning(i18n("The target dir does not exist"));
Martin Koller's avatar
Martin Koller committed
476 477 478 479 480
        return false;
      }
    }
  }

Martin Koller's avatar
Martin Koller committed
481 482 483 484
  excludeDirs.clear();
  excludeFiles.clear();

  // build map for directories and files to be excluded for fast lookup
485
  for (const QString &name : excludes)
Martin Koller's avatar
Martin Koller committed
486
  {
Martin Koller's avatar
Martin Koller committed
487
    QFileInfo info(name);
Martin Koller's avatar
Martin Koller committed
488 489

    if ( !info.isSymLink() && info.isDir() )
Martin Koller's avatar
Martin Koller committed
490
      excludeDirs.insert(name);
Martin Koller's avatar
Martin Koller committed
491
    else
Martin Koller's avatar
Martin Koller committed
492
      excludeFiles.insert(name);
Martin Koller's avatar
Martin Koller committed
493 494
  }

495
  baseName = QString();
Martin Koller's avatar
Martin Koller committed
496 497 498
  sliceNum = 0;
  totalBytes = 0;
  totalFiles = 0;
Martin Koller's avatar
Martin Koller committed
499
  filteredFiles = 0;
Martin Koller's avatar
Martin Koller committed
500
  cancelled = false;
501
  skippedFiles = false;
Martin Koller's avatar
Martin Koller committed
502
  sliceList.clear();
Martin Koller's avatar
Martin Koller committed
503

Martin Koller's avatar
Martin Koller committed
504 505
  QDateTime startTime = QDateTime::currentDateTime();

Martin Koller's avatar
Martin Koller committed
506
  runs = true;
507
  Q_EMIT inProgress(true);
Martin Koller's avatar
Martin Koller committed
508

Martin Koller's avatar
Martin Koller committed
509 510 511
  QTimer runTimer;
  if ( interactive )  // else we do not need to be interrupted during the backup
  {
Laurent Montel's avatar
Laurent Montel committed
512
    connect(&runTimer, &QTimer::timeout, this, &Archiver::updateElapsed);
Martin Koller's avatar
Martin Koller committed
513 514 515 516
    runTimer.start(1000);
  }
  elapsed.start();

517 518 519
  if ( ! getNextSlice() )
  {
    runs = false;
520
    Q_EMIT inProgress(false);
521 522 523 524

    return false;
  }

Martin Koller's avatar
Martin Koller committed
525
  for (QStringList::const_iterator it = includes.constBegin(); !cancelled && (it != includes.constEnd()); ++it)
Martin Koller's avatar
Martin Koller committed
526 527 528
  {
    QString entry = *it;

Martin Koller's avatar
Martin Koller committed
529
    if ( (entry.length() > 1) && entry.endsWith(QLatin1Char('/')) )
530
      entry.chop(1);
Martin Koller's avatar
Martin Koller committed
531 532 533 534 535

    QFileInfo info(entry);

    if ( !info.isSymLink() && info.isDir() )
    {
Martin Koller's avatar
Martin Koller committed
536
      QDir dir(info.absoluteFilePath());
Martin Koller's avatar
Martin Koller committed
537 538 539
      addDirFiles(dir);
    }
    else
Martin Koller's avatar
Martin Koller committed
540
      addFile(info.absoluteFilePath());
Martin Koller's avatar
Martin Koller committed
541 542 543 544
  }

  finishSlice();

Martin Koller's avatar
Martin Koller committed
545 546 547
  // reduce the number of old backups to the defined number
  if ( !cancelled && (numKeptBackups != UNLIMITED) )
  {
548
    Q_EMIT logging(i18n("...reducing number of kept archives to max. %1", numKeptBackups));
Martin Koller's avatar
Martin Koller committed
549

550 551 552 553
    if ( !targetURL.isLocalFile() )  // KIO needs $DISPLAY; non-interactive only allowed for local targets
    {
      QPointer<KIO::ListJob> listJob;
      listJob = KIO::listDir(targetURL, KIO::DefaultFlags, false);
Martin Koller's avatar
Martin Koller committed
554

Laurent Montel's avatar
Laurent Montel committed
555 556
      connect(listJob.data(), &KIO::ListJob::entries,
              this, &Archiver::slotListResult);
Martin Koller's avatar
Martin Koller committed
557

558 559 560 561 562 563 564
      while ( listJob )
        qApp->processEvents(QEventLoop::WaitForMoreEvents);
    }
    else  // non-intercative. create UDSEntryList on our own
    {
      QDir dir(targetURL.path());
      targetDirList.clear();
565 566
      const auto entryList = dir.entryList();
      for (const QString &fileName : entryList)
567 568
      {
        KIO::UDSEntry entry;
569 570 571
#if (KIO_VERSION >= QT_VERSION_CHECK(5, 48, 0))
        entry.fastInsert(KIO::UDSEntry::UDS_NAME, fileName);
#else
572
        entry.insert(KIO::UDSEntry::UDS_NAME, fileName);
573
#endif
574 575 576 577
        targetDirList.append(entry);
      }
      jobResult = 0;
    }
Martin Koller's avatar
Martin Koller committed
578 579 580

    if ( jobResult == 0 )
    {
Laurent Montel's avatar
Laurent Montel committed
581
      std::sort(targetDirList.begin(), targetDirList.end(), Archiver::UDSlessThan);
Laurent Montel's avatar
Laurent Montel committed
582
      QString prefix = filePrefix.isEmpty() ? QStringLiteral("backup_") : (filePrefix + QLatin1String("_"));
Martin Koller's avatar
Martin Koller committed
583 584 585 586

      QString sliceName;
      int num = 0;

587
      for (const KIO::UDSEntry &entry : qAsConst(targetDirList))
Martin Koller's avatar
Martin Koller committed
588 589 590 591
      {
        QString entryName = entry.stringValue(KIO::UDSEntry::UDS_NAME);

        if ( entryName.startsWith(prefix) &&  // only matching current profile
592
             entryName.endsWith(QLatin1String(".tar")) )     // just to be sure
Martin Koller's avatar
Martin Koller committed
593 594 595 596 597 598
        {
          if ( (num < numKeptBackups) &&
               (sliceName.isEmpty() ||
                !entryName.startsWith(sliceName)) )     // whenever a new backup set (different time) is found
          {
            sliceName = entryName.left(prefix.length() + strlen("yyyy.MM.dd-hh.mm.ss_"));
599
            if ( !entryName.endsWith(QLatin1String("_inc.tar")) )  // do not count partial (differential) backup files
Martin Koller's avatar
Martin Koller committed
600
              num++;
Martin Koller's avatar
Martin Koller committed
601 602 603 604 605 606
            if ( num == numKeptBackups ) num++;  // from here on delete all others
          }

          if ( (num > numKeptBackups) &&   // delete all other files
               !entryName.startsWith(sliceName) )     // keep complete last matching archive set
          {
Martin Koller's avatar
Martin Koller committed
607 608
            QUrl url = targetURL;
            url = url.adjusted(QUrl::StripTrailingSlash);
609
            url.setPath(url.path() + QLatin1Char('/') + entryName);
610
            Q_EMIT logging(i18n("...deleting %1", entryName));
Martin Koller's avatar
Martin Koller committed
611 612

            // delete the file using KIO
613
            if ( !targetURL.isLocalFile() )  // KIO needs $DISPLAY; non-interactive only allowed for local targets
Martin Koller's avatar
Martin Koller committed
614 615
            {
              QPointer<KIO::SimpleJob> delJob;
616
              delJob = KIO::file_delete(url, KIO::DefaultFlags);
Martin Koller's avatar
Martin Koller committed
617

Laurent Montel's avatar
Laurent Montel committed
618
              connect(delJob.data(), &KJob::result, this, &Archiver::slotResult);
Martin Koller's avatar
Martin Koller committed
619 620 621 622

              while ( delJob )
                qApp->processEvents(QEventLoop::WaitForMoreEvents);
            }
623 624 625 626 627
            else
            {
              QDir dir(targetURL.path());
              dir.remove(entryName);
            }
Martin Koller's avatar
Martin Koller committed
628 629 630 631 632 633
          }
        }
      }
    }
    else
    {
634
      Q_EMIT warning(i18n("fetching directory listing of target failed. Can not reduce kept archives."));
Martin Koller's avatar
Martin Koller committed
635 636 637
    }
  }

Martin Koller's avatar
Martin Koller committed
638
  runs = false;
639
  Q_EMIT inProgress(false);
Martin Koller's avatar
Martin Koller committed
640 641
  runTimer.stop();
  updateElapsed();  // to catch the last partly second
Martin Koller's avatar
Martin Koller committed
642

Martin Koller's avatar
Martin Koller committed
643
  if ( !cancelled )
Martin Koller's avatar
Martin Koller committed
644
  {
Martin Koller's avatar
Martin Koller committed
645 646
    lastBackup = startTime;
    if ( !isIncrementalBackup() )
Martin Koller's avatar
Martin Koller committed
647
    {
Martin Koller's avatar
Martin Koller committed
648
      lastFullBackup = lastBackup;
Martin Koller's avatar
Martin Koller committed
649 650
      setIncrementalBackup(fullBackupInterval > 1);  // after a full backup, the next will be incremental
    }
Martin Koller's avatar
Martin Koller committed
651 652 653 654 655 656

    if ( (fullBackupInterval > 1) && !loadedProfile.isEmpty() )
    {
      QString error;
      if ( !saveProfile(loadedProfile, includes, excludes, error) )
      {
657
        Q_EMIT warning(i18n("Could not write backup timestamps into profile %1: %2", loadedProfile, error));
Martin Koller's avatar
Martin Koller committed
658 659 660
      }
    }

661
    Q_EMIT logging(i18n("-- Filtered Files: %1", filteredFiles));
Martin Koller's avatar
Martin Koller committed
662

663
    if ( skippedFiles )
664
      Q_EMIT logging(i18n("!! Backup finished <b>but files were skipped</b> !!"));
665
    else
666
      Q_EMIT logging(i18n("-- Backup successfully finished --"));
667

668 669
    if ( interactive )
    {
Martin Koller's avatar
Martin Koller committed
670
      int ret = KMessageBox::questionYesNoList(static_cast<QWidget*>(parent()),
671 672 673 674 675
                               skippedFiles ?
                                 i18n("The backup has finished but files were skipped.\n"
                                      "What do you want to do now?") :
                                 i18n("The backup has finished successfully.\n"
                                      "What do you want to do now?"),
Martin Koller's avatar
Martin Koller committed
676
                               sliceList,
Laurent Montel's avatar
Laurent Montel committed
677
                               QString(),
Martin Koller's avatar
Martin Koller committed
678
                               KStandardGuiItem::cont(), KStandardGuiItem::quit(),
Laurent Montel's avatar
Laurent Montel committed
679
                               QStringLiteral("showDoneInfo"));
680 681 682 683

      if ( ret == KMessageBox::No ) // quit
        qApp->quit();
    }
Martin Koller's avatar
Martin Koller committed
684 685
    else
    {
Martin Koller's avatar
Martin Koller committed
686
      std::cerr << "-------" << std::endl;
687
      for (const QString &slice : qAsConst(sliceList)) {
Martin Koller's avatar
Martin Koller committed
688
        std::cerr << slice.toUtf8().constData() << std::endl;
689
      }
Martin Koller's avatar
Martin Koller committed
690 691
      std::cerr << "-------" << std::endl;

692 693 694
      std::cerr << i18n("Totals: Files: %1, Size: %2, Duration: %3",
                   totalFiles,
                   KIO::convertSize(totalBytes),
695
                   QTime(0, 0).addMSecs(elapsed.elapsed()).toString(QStringLiteral("HH:mm:ss")))
Martin Koller's avatar
Martin Koller committed
696 697
                   .toUtf8().constData() << std::endl;
    }
698

699
    return true;
Martin Koller's avatar
Martin Koller committed
700
  }
Martin Koller's avatar
Martin Koller committed
701
  else
702
  {
703
    Q_EMIT logging(i18n("...Backup aborted!"));
704 705
    return false;
  }
Martin Koller's avatar
Martin Koller committed
706 707 708 709 710 711
}

//--------------------------------------------------------------------------------

void Archiver::cancel()
{
Martin Koller's avatar
Martin Koller committed
712 713
  if ( !runs ) return;

Martin Koller's avatar
Martin Koller committed
714 715 716
  if ( job )
  {
    job->kill();
Laurent Montel's avatar
Laurent Montel committed
717
    job = nullptr;
Martin Koller's avatar
Martin Koller committed
718 719 720 721
  }
  if ( !cancelled )
  {
    cancelled = true;
Martin Koller's avatar
Martin Koller committed
722 723 724 725 726

    if ( archive )
    {
      archive->close();  // else I can not remove the file - don't know why
      delete archive;
Laurent Montel's avatar
Laurent Montel committed
727
      archive = nullptr;
Martin Koller's avatar
Martin Koller committed
728 729
    }

Martin Koller's avatar
Martin Koller committed
730
    QFile(archiveName).remove(); // remove the unfinished tar file (which is now corrupted)
731
    Q_EMIT warning(i18n("Backup cancelled"));
Martin Koller's avatar
Martin Koller committed
732 733 734 735 736 737 738 739 740 741
  }
}

//--------------------------------------------------------------------------------

void Archiver::finishSlice()
{
  if ( archive )
    archive->close();

742
  if ( ! cancelled )
Martin Koller's avatar
Martin Koller committed
743
  {
744
    runScript(QStringLiteral("slice_closed"));
Martin Koller's avatar
Martin Koller committed
745

746
    if ( targetURL.isLocalFile() )
Martin Koller's avatar
Martin Koller committed
747
    {
748
      Q_EMIT logging(i18n("...finished slice %1", archiveName));
Martin Koller's avatar
Martin Koller committed
749 750 751
      sliceList << archiveName;  // store name for display at the end
    }
    else
Martin Koller's avatar
Martin Koller committed
752
    {
Martin Koller's avatar
Martin Koller committed
753 754
      QUrl source = QUrl::fromLocalFile(archiveName);
      QUrl target = targetURL;
Martin Koller's avatar
Martin Koller committed
755

Martin Koller's avatar
Martin Koller committed
756 757 758
      while ( true )
      {
        // copy to have the archive for the script later down
759
        job = KIO::copy(source, target, KIO::DefaultFlags);
Martin Koller's avatar
Martin Koller committed
760

Laurent Montel's avatar
Laurent Montel committed
761
        connect(job.data(), &KJob::result, this, &Archiver::slotResult);
Martin Koller's avatar
Martin Koller committed
762

763
        Q_EMIT logging(i18n("...uploading archive %1 to %2", source.fileName(), target.toString()));
Martin Koller's avatar
Martin Koller committed
764

Martin Koller's avatar
Martin Koller committed
765 766 767 768
        while ( job )
          qApp->processEvents(QEventLoop::WaitForMoreEvents);

        if ( jobResult == 0 )
Martin Koller's avatar
Martin Koller committed
769
        {
Martin Koller's avatar
Martin Koller committed
770
          target = target.adjusted(QUrl::StripTrailingSlash);
771
          target.setPath(target.path() + QLatin1Char('/') + source.fileName());
Martin Koller's avatar
Martin Koller committed
772
          sliceList << target.toLocalFile();  // store name for display at the end
Martin Koller's avatar
Martin Koller committed
773 774
          break;
        }
775 776
        else
        {
777 778
          enum { ASK, CANCEL, RETRY } action = ASK;
          while ( action == ASK )
Martin Koller's avatar
Martin Koller committed
779
          {
780
            int ret = KMessageBox::warningYesNoCancel(static_cast<QWidget*>(parent()),
Martin Koller's avatar
Martin Koller committed
781
                        i18n("How shall we proceed with the upload?"), i18n("Upload Failed"),
782 783 784 785 786 787 788 789
                        KGuiItem(i18n("Retry")), KGuiItem(i18n("Change Target")));

            if ( ret == KMessageBox::Cancel )
            {
              action = CANCEL;
              break;
            }
            else if ( ret == KMessageBox::No )  // change target
790
            {
Martin Koller's avatar
Martin Koller committed
791
              target = QFileDialog::getExistingDirectoryUrl(static_cast<QWidget*>(parent()));
792 793
              if ( target.isEmpty() )
                action = ASK;
794 795 796
              else
                action = RETRY;
            }
797 798
            else
              action = RETRY;
Martin Koller's avatar
Martin Koller committed
799
          }
800 801 802

          if ( action == CANCEL )
            break;
803
        }
Martin Koller's avatar
Martin Koller committed
804 805
      }

Martin Koller's avatar
Martin Koller committed
806 807 808
      if ( jobResult != 0 )
        cancel();
    }
Martin Koller's avatar
Martin Koller committed
809 810 811
  }

  if ( ! cancelled )
812
    runScript(QStringLiteral("slice_finished"));
Martin Koller's avatar
Martin Koller committed
813 814 815 816 817

  if ( !targetURL.isLocalFile() )
    QFile(archiveName).remove(); // remove the tmp file

  delete archive;
Laurent Montel's avatar
Laurent Montel committed
818
  archive = nullptr;
Martin Koller's avatar
Martin Koller committed
819 820 821 822
}

//--------------------------------------------------------------------------------

Martin Koller's avatar
Martin Koller committed
823
void Archiver::slotResult(KJob *theJob)
Martin Koller's avatar
Martin Koller committed
824
{
Martin Koller's avatar
Martin Koller committed
825 826
  if ( (jobResult = theJob->error()) )
  {
827
    theJob->uiDelegate()->showErrorMessage();
Martin Koller's avatar
Martin Koller committed
828

829
    Q_EMIT warning(theJob->errorString());
Martin Koller's avatar
Martin Koller committed
830 831 832 833 834 835 836 837 838
  }
}

//--------------------------------------------------------------------------------

void Archiver::slotListResult(KIO::Job *theJob, const KIO::UDSEntryList &entries)
{
  if ( (jobResult = theJob->error()) )
  {
839
    theJob->uiDelegate()->showErrorMessage();
Martin Koller's avatar
Martin Koller committed
840

841
    Q_EMIT warning(theJob->errorString());
Martin Koller's avatar
Martin Koller committed
842
  }
Martin Koller's avatar
Martin Koller committed
843

Martin Koller's avatar
Martin Koller committed
844
  targetDirList = entries;
Martin Koller's avatar
Martin Koller committed
845 846 847 848 849 850 851 852 853 854 855
}

//--------------------------------------------------------------------------------

void Archiver::runScript(const QString &mode)
{
  // do some extra action via external script (program)
  if ( sliceScript.length() )
  {
    QString mountPoint;
    if ( targetURL.isLocalFile() )
Martin Koller's avatar
Martin Koller committed
856 857
    {
      KMountPoint::Ptr ptr = KMountPoint::currentMountPoints().findByPath(targetURL.path());
Martin Koller's avatar
Martin Koller committed
858
      if ( ptr )
Martin Koller's avatar
Martin Koller committed
859 860
        mountPoint = ptr->mountPoint();
    }
Martin Koller's avatar
Martin Koller committed
861

Martin Koller's avatar
Martin Koller committed
862 863 864
    KProcess proc;
    proc << sliceScript
         << mode
Martin Koller's avatar
Martin Koller committed
865 866 867
         << archiveName
         << targetURL.toString(QUrl::PreferLocalFile)
         << mountPoint;
Martin Koller's avatar
Martin Koller committed
868

Martin Koller's avatar
Martin Koller committed
869 870
    connect(&proc, &KProcess::readyReadStandardOutput,
            this, &Archiver::receivedOutput);
Martin Koller's avatar
Martin Koller committed
871

Martin Koller's avatar
Martin Koller committed
872 873 874
    proc.setOutputChannelMode(KProcess::MergedChannels);

    if ( proc.execute() == -2 )
Martin Koller's avatar
Martin Koller committed
875
    {
876
      QString message = i18n("The script '%1' could not be started.", sliceScript);
877 878 879
      if ( interactive )
        KMessageBox::error(static_cast<QWidget*>(parent()), message);
      else
880
        Q_EMIT warning(message);
Martin Koller's avatar
Martin Koller committed
881 882 883 884 885 886
    }
  }
}

//--------------------------------------------------------------------------------

Martin Koller's avatar