gducreatediskimagedialog.c 36.8 KB
Newer Older
1 2
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
David Zeuthen's avatar
David Zeuthen committed
3
 * Copyright (C) 2008-2013 Red Hat, Inc.
4
 *
5
 * Licensed under GPL version 2 or later.
6
 *
David Zeuthen's avatar
David Zeuthen committed
7
 * Author: David Zeuthen <zeuthen@gmail.com>
8 9 10 11
 */

#include "config.h"

12 13 14
#define _GNU_SOURCE
#include <fcntl.h>

15 16 17
#include <glib/gi18n.h>
#include <gio/gunixfdlist.h>
#include <gio/gunixinputstream.h>
18
#include <gio/gfiledescriptorbased.h>
19

20 21 22 23
#include <glib-unix.h>
#include <sys/ioctl.h>
#include <linux/fs.h>

24 25
#include <canberra-gtk.h>

26 27 28 29
#include "gduapplication.h"
#include "gduwindow.h"
#include "gducreatediskimagedialog.h"
#include "gduvolumegrid.h"
30
#include "gduestimator.h"
31
#include "gdulocaljob.h"
32

33 34
#include "gdudvdsupport.h"

35 36 37 38 39 40 41 42 43 44 45
/* TODOs / ideas for Disk Image creation
 *
 * - Be tolerant of I/O errors like dd_rescue(1), see http://www.gnu.org/s/ddrescue/ddrescue.html
 * - Create images useful for Virtualization, e.g. vdi, vmdk, qcow2. Maybe use libguestfs for
 *   this. See http://libguestfs.org/
 * - Support a Apple DMG-ish format
 * - Sliding buffer size
 * - Update time remaining / speed exactly every 1/10th second instead of when we've read a full buffer
 *
 */

46 47 48 49 50 51 52 53 54 55 56 57 58 59
/* ---------------------------------------------------------------------------------------------------- */

typedef struct
{
  volatile gint ref_count;

  GduWindow *window;
  UDisksObject *object;
  UDisksBlock *block;
  UDisksDrive *drive;

  GtkBuilder *builder;
  GtkWidget *dialog;

60 61 62 63 64 65 66 67
  GtkWidget *source_label;
  GtkWidget *name_label;
  GtkWidget *name_entry;
  GtkWidget *folder_label;
  GtkWidget *folder_fcbutton;

  GtkWidget *start_copying_button;
  GtkWidget *cancel_button;
68 69 70 71

  GCancellable *cancellable;
  GFile *output_file;
  GFileOutputStream *output_file_stream;
72

73 74
  /* must hold copy_lock when reading/writing these */
  GMutex copy_lock;
75
  GduEstimator *estimator;
76

77
  gboolean allocating_file;
78
  gboolean retrieving_dvd_keys;
79 80 81
  guint64 num_error_bytes;
  gint64 start_time_usec;
  gint64 end_time_usec;
82
  gboolean played_read_error_sound;
83 84 85 86

  guint update_id;
  GError *copy_error;

87 88
  gulong response_signal_handler_id;
  gboolean completed;
89 90

  guint inhibit_cookie;
91 92

  GduLocalJob *local_job;
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
} DialogData;

static const struct {
  goffset offset;
  const gchar *name;
} widget_mapping[] = {
  {G_STRUCT_OFFSET (DialogData, source_label), "source-label"},
  {G_STRUCT_OFFSET (DialogData, name_label), "name-label"},
  {G_STRUCT_OFFSET (DialogData, name_entry), "name-entry"},
  {G_STRUCT_OFFSET (DialogData, folder_label), "folder-label"},
  {G_STRUCT_OFFSET (DialogData, folder_fcbutton), "folder-fcbutton"},

  {G_STRUCT_OFFSET (DialogData, start_copying_button), "start-copying-button"},
  {G_STRUCT_OFFSET (DialogData, cancel_button), "cancel-button"},
  {0, NULL}
};
109

110 111
/* ---------------------------------------------------------------------------------------------------- */

112 113
static DialogData *
dialog_data_ref (DialogData *data)
114 115 116 117 118
{
  g_atomic_int_inc (&data->ref_count);
  return data;
}

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
static void
dialog_data_terminate_job (DialogData *data)
{
  if (data->local_job != NULL)
    {
      gdu_application_destroy_local_job (gdu_window_get_application (data->window), data->local_job);
      data->local_job = NULL;
    }
}

static void
dialog_data_uninhibit (DialogData *data)
{
  if (data->inhibit_cookie > 0)
    {
      gtk_application_uninhibit (GTK_APPLICATION (gdu_window_get_application (data->window)),
                                 data->inhibit_cookie);
      data->inhibit_cookie = 0;
    }
}

static void
dialog_data_hide (DialogData *data)
{
  if (data->dialog != NULL)
    {
      GtkWidget *dialog;
      if (data->response_signal_handler_id != 0)
        g_signal_handler_disconnect (data->dialog, data->response_signal_handler_id);
      dialog = data->dialog;
      data->dialog = NULL;
      gtk_widget_hide (dialog);
      gtk_widget_destroy (dialog);
      data->dialog = NULL;
    }
}

156
static void
157
dialog_data_unref (DialogData *data)
158 159 160
{
  if (g_atomic_int_dec_and_test (&data->ref_count))
    {
161 162 163 164
      dialog_data_terminate_job (data);
      dialog_data_uninhibit (data);
      dialog_data_hide (data);

165 166 167 168 169 170 171 172
      g_clear_object (&data->cancellable);
      g_clear_object (&data->output_file_stream);
      g_object_unref (data->window);
      g_object_unref (data->object);
      g_object_unref (data->block);
      g_clear_object (&data->drive);
      if (data->builder != NULL)
        g_object_unref (data->builder);
173
      g_clear_object (&data->estimator);
174
      g_mutex_clear (&data->copy_lock);
175 176 177 178
      g_free (data);
    }
}

179 180 181 182 183 184 185 186 187 188 189 190 191 192
static gboolean
unref_in_idle (gpointer user_data)
{
  DialogData *data = user_data;
  dialog_data_unref (data);
  return FALSE; /* remove source */
}

static void
dialog_data_unref_in_idle (DialogData *data)
{
  g_idle_add (unref_in_idle, data);
}

193 194 195
/* ---------------------------------------------------------------------------------------------------- */

static void
196
dialog_data_complete_and_unref (DialogData *data)
197
{
198 199 200 201 202
  if (!data->completed)
    {
      data->completed = TRUE;
      g_cancellable_cancel (data->cancellable);
    }
203 204
  dialog_data_uninhibit (data);
  dialog_data_hide (data);
205
  dialog_data_unref (data);
206 207
}

208 209 210
/* ---------------------------------------------------------------------------------------------------- */

static void
211
create_disk_image_update (DialogData *data)
212 213 214
{
  gboolean can_proceed = FALSE;

215
  if (strlen (gtk_entry_get_text (GTK_ENTRY (data->name_entry))) > 0)
216 217 218 219 220 221 222 223 224 225
    can_proceed = TRUE;

  gtk_dialog_set_response_sensitive (GTK_DIALOG (data->dialog), GTK_RESPONSE_OK, can_proceed);
}

static void
on_notify (GObject     *object,
           GParamSpec  *pspec,
           gpointer     user_data)
{
226
  DialogData *data = user_data;
227 228 229 230
  create_disk_image_update (data);
}


231 232 233
/* ---------------------------------------------------------------------------------------------------- */

static void
234
create_disk_image_populate (DialogData *data)
235
{
236
  UDisksObjectInfo *info = NULL;
237 238
  gchar *device_name;
  gchar *now_string;
239
  gchar *proposed_filename = NULL;
240 241 242
  guint n;
  GTimeZone *tz;
  GDateTime *now;
243 244
  const gchar *fstype;
  const gchar *fslabel;
245 246 247 248 249 250 251 252 253 254 255 256 257 258

  device_name = udisks_block_dup_preferred_device (data->block);
  if (g_str_has_prefix (device_name, "/dev/"))
    memmove (device_name, device_name + 5, strlen (device_name) - 5 + 1);
  for (n = 0; device_name[n] != '\0'; n++)
    {
      if (device_name[n] == '/')
        device_name[n] = '_';
    }

  tz = g_time_zone_new_local ();
  now = g_date_time_new_now (tz);
  now_string = g_date_time_format (now, "%Y-%m-%d %H%M");

259 260 261
  /* If it's an ISO/UDF filesystem, suggest a filename ending in .iso */
  fstype = udisks_block_get_id_type (data->block);
  fslabel = udisks_block_get_id_label (data->block);
262
  if (g_strcmp0 (fstype, "iso9660") == 0 || g_strcmp0 (fstype, "udf") == 0)
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
    {
      if (fslabel != NULL && strlen (fslabel) > 0)
        proposed_filename = g_strdup_printf ("%s.iso", fslabel);
    }

  if (proposed_filename == NULL)
    {
      /* Translators: The suggested name for the disk image to create.
       *              The first %s is a name for the disk (e.g. 'sdb').
       *              The second %s is today's date and time, e.g. "March 2, 1976 6:25AM".
       */
      proposed_filename = g_strdup_printf (_("Disk Image of %s (%s).img"),
                                           device_name,
                                           now_string);
    }

279
  gtk_entry_set_text (GTK_ENTRY (data->name_entry), proposed_filename);
280
  g_free (proposed_filename);
281 282 283 284 285
  g_free (device_name);
  g_date_time_unref (now);
  g_time_zone_unref (tz);
  g_free (now_string);

286 287 288
  gdu_utils_configure_file_chooser_for_disk_images (GTK_FILE_CHOOSER (data->folder_fcbutton),
                                                    FALSE,   /* set file types */
                                                    FALSE);  /* allow_compressed */
289 290 291

  /* Source label */
  info = udisks_client_get_object_info (gdu_window_get_client (data->window), data->object);
292 293
  gtk_label_set_text (GTK_LABEL (data->source_label), udisks_object_info_get_one_liner (info));
  g_clear_object (&info);
294 295 296
}

/* ---------------------------------------------------------------------------------------------------- */
297

298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
static void
play_read_error_sound (DialogData *data)
{
  const gchar *sound_message;

  /* Translators: A descriptive string for the sound played when
   * there's a read error that's being ignored, see
   * CA_PROP_EVENT_DESCRIPTION
   */
  sound_message = _("Disk image read error");
  ca_gtk_play_for_widget (GTK_WIDGET (data->window), 0,
                          CA_PROP_EVENT_ID, "dialog-warning",
                          CA_PROP_EVENT_DESCRIPTION, sound_message,
                          NULL);
}

/* ---------------------------------------------------------------------------------------------------- */

316
static void
317
update_job (DialogData *data,
318
            gboolean    done)
319
{
320
  gchar *extra_markup = NULL;
321
  guint64 bytes_completed = 0;
322
  guint64 bytes_target = 0;
323
  guint64 bytes_per_sec = 0;
324
  guint64 usec_remaining = 0;
325
  guint64 num_error_bytes = 0;
326 327
  gdouble progress = 0.0;
  gchar *s2, *s3;
328

329 330
  g_mutex_lock (&data->copy_lock);
  if (data->estimator != NULL)
331
    {
332 333 334 335 336
      bytes_per_sec = gdu_estimator_get_bytes_per_sec (data->estimator);
      usec_remaining = gdu_estimator_get_usec_remaining (data->estimator);
      bytes_completed = gdu_estimator_get_completed_bytes (data->estimator);
      bytes_target = gdu_estimator_get_target_bytes (data->estimator);
      num_error_bytes = data->num_error_bytes;
337
    }
338 339
  data->update_id = 0;
  g_mutex_unlock (&data->copy_lock);
340

341 342 343 344 345
  if (data->allocating_file)
    {
      extra_markup = g_strdup (_("Allocating Disk Image"));
    }
  else if (data->retrieving_dvd_keys)
346
    {
347
      extra_markup = g_strdup (_("Retrieving DVD keys"));
348
    }
349 350 351 352 353 354 355

  if (num_error_bytes > 0)
    {
      s2 = g_format_size (num_error_bytes);
      /* Translators: Shown when there are read errors and we skip some data.
       *              The first %s is the amount of unreadable data (ex. "512 kB").
       */
356
      s3 = g_strdup_printf (_("%s unreadable (replaced with zeroes)"), s2);
357 358 359
      /* TODO: once https://bugzilla.gnome.org/show_bug.cgi?id=657194 is resolved, use that instead
       * of hard-coding the color
       */
360 361
      g_free (extra_markup);
      extra_markup = g_strdup_printf ("<span foreground=\"#ff0000\">%s</span>", s3);
362 363 364 365
      g_free (s3);
      g_free (s2);
    }

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
  if (data->local_job != NULL)
    {
      udisks_job_set_bytes (UDISKS_JOB (data->local_job), bytes_target);
      udisks_job_set_rate (UDISKS_JOB (data->local_job), bytes_per_sec);

      if (done)
        {
          progress = 1.0;
        }
      else
        {
          if (bytes_target != 0)
            progress = ((gdouble) bytes_completed) / ((gdouble) bytes_target);
          else
            progress = 0.0;
        }
      udisks_job_set_progress (UDISKS_JOB (data->local_job), progress);
383

384 385 386 387 388 389 390 391
      if (usec_remaining == 0)
        udisks_job_set_expected_end_time (UDISKS_JOB (data->local_job), 0);
      else
        udisks_job_set_expected_end_time (UDISKS_JOB (data->local_job), usec_remaining + g_get_real_time ());

      gdu_local_job_set_extra_markup (data->local_job, extra_markup);
    }

392 393 394 395 396 397 398
  /* Play a sound the first time we encounter a read error */
  if (num_error_bytes > 0 && !data->played_read_error_sound)
    {
      play_read_error_sound (data);
      data->played_read_error_sound = TRUE;
    }

399
  g_free (extra_markup);
400 401
}

402 403 404
/* ---------------------------------------------------------------------------------------------------- */

static void
405
play_complete_sound (DialogData *data)
406 407
{
  const gchar *sound_message;
408

409
  /* Translators: A descriptive string for the 'complete' sound, see CA_PROP_EVENT_DESCRIPTION */
410
  sound_message = _("Disk image copying complete");
411
  ca_gtk_play_for_widget (GTK_WIDGET (data->window), 0,
412 413 414 415 416 417 418
                          CA_PROP_EVENT_ID, "complete",
                          CA_PROP_EVENT_DESCRIPTION, sound_message,
                          NULL);
}

/* ---------------------------------------------------------------------------------------------------- */

419
static gboolean
420
on_update_job (gpointer user_data)
421
{
422
  DialogData *data = user_data;
423
  update_job (data, FALSE);
424 425
  dialog_data_unref (data);
  return FALSE; /* remove source */
426 427
}

428 429
/* ---------------------------------------------------------------------------------------------------- */

430 431
static gboolean
on_show_error (gpointer user_data)
432
{
433
  DialogData *data = user_data;
434

435
  dialog_data_uninhibit (data);
436

437
  g_assert (data->copy_error != NULL);
438
  gdu_utils_show_error (GTK_WINDOW (data->window),
439 440 441
                        _("Error creating disk image"),
                        data->copy_error);
  g_clear_error (&data->copy_error);
442

443
  dialog_data_complete_and_unref (data);
444

445 446 447
  dialog_data_unref (data);
  return FALSE; /* remove source */
}
448

449
/* ---------------------------------------------------------------------------------------------------- */
450

451 452 453 454 455
static gboolean
on_success (gpointer user_data)
{
  DialogData *data = user_data;

456
  update_job (data, TRUE);
457

458 459 460
  play_complete_sound (data);
  dialog_data_uninhibit (data);
  dialog_data_complete_and_unref (data);
461

462 463 464 465 466 467
  /* OK, we're done but we had to replace unreadable data with
   * zeroes. Bring up a modal dialog to inform the user of this and
   * allow him to delete the file, if so desired.
   */
  if (data->num_error_bytes > 0)
    {
468
      GtkWidget *dialog;
469 470 471 472 473 474 475 476
      GError *error = NULL;
      gchar *s = NULL;
      gint response;
      gdouble percentage;

      dialog = gtk_message_dialog_new_with_markup (GTK_WINDOW (data->window),
                                                   GTK_DIALOG_MODAL,
                                                   GTK_MESSAGE_WARNING,
477
                                                   GTK_BUTTONS_NONE,
478
                                                   "<big><b>%s</b></big>",
479 480
                                                   /* Translators: Primary message in dialog shown if some data was unreadable while creating a disk image */
                                                   _("Unrecoverable read errors while creating disk image"));
481
      s = g_format_size (data->num_error_bytes);
482
      percentage = 100.0 * ((gdouble) data->num_error_bytes) / ((gdouble) gdu_estimator_get_target_bytes (data->estimator));
483
      gtk_message_dialog_format_secondary_markup (GTK_MESSAGE_DIALOG (dialog),
484
                                                  /* Translators: Secondary message in dialog shown if some data was unreadable while creating a disk image.
485
                                                   * The %f is the percentage of unreadable data (ex. 13.0).
486 487
                                                   * The first %s is the amount of unreadable data (ex. "4.2 MB").
                                                   * The second %s is the name of the device (ex "/dev/").
488
                                                   */
489
                                                  _("%2.1f%% (%s) of the data on the device “%s” was unreadable and replaced with zeroes in the created disk image file. This typically happens if the medium is scratched or if there is physical damage to the drive"),
490
                                                  percentage,
491 492
                                                  s,
                                                  gtk_label_get_text (GTK_LABEL (data->source_label)));
493 494 495 496 497
      gtk_dialog_add_button (GTK_DIALOG (dialog),
                             /* Translators: Label of secondary button in dialog if some data was unreadable while creating a disk image */
                             _("_Delete Disk Image File"),
                             GTK_RESPONSE_NO);
      gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Close"), GTK_RESPONSE_CLOSE);
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
      gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CLOSE);
      response = gtk_dialog_run (GTK_DIALOG (dialog));
      gtk_widget_destroy (dialog);
      g_free (s);

      if (response == GTK_RESPONSE_NO)
        {
          if (!g_file_delete (data->output_file, NULL, &error))
            {
              g_warning ("Error deleting file: %s (%s, %d)",
                         error->message, g_quark_to_string (error->domain), error->code);
              g_clear_error (&error);
            }
        }
    }

514 515
  dialog_data_unref (data);
  return FALSE; /* remove source */
516 517
}

518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
/* ---------------------------------------------------------------------------------------------------- */

/* Note that error on reading is *not* considered an error - instead 0
 * is returned.
 *
 * Error conditions include failure to seek or write to output.
 *
 * Returns: Number of bytes actually read (e.g. not include padding) -1 if @error is set.
 */
static gssize
copy_span (int              fd,
           GOutputStream   *output_stream,
           guint64          offset,
           guint64          size,
           guchar          *buffer,
           gboolean         pad_with_zeroes,
534
           GduDVDSupport   *dvd_support,
535 536
           GCancellable    *cancellable,
           GError         **error)
537
{
538 539 540 541 542 543 544 545 546 547
  gint64 ret = -1;
  ssize_t num_bytes_read;
  gsize num_bytes_to_write;

  g_return_val_if_fail (-1, buffer != NULL);
  g_return_val_if_fail (-1, G_IS_OUTPUT_STREAM (output_stream));
  g_return_val_if_fail (-1, buffer != NULL);
  g_return_val_if_fail (-1, cancellable == NULL || G_IS_CANCELLABLE (cancellable));
  g_return_val_if_fail (-1, error == NULL || *error == NULL);

548
  if (dvd_support != NULL)
549
    {
550
      num_bytes_read = gdu_dvd_support_read (dvd_support, fd, buffer, offset, size);
551 552 553
    }
  else
    {
554
      if (lseek (fd, offset, SEEK_SET) == (off_t) -1)
555 556
        {
          g_set_error (error,
557 558 559
                       G_IO_ERROR, g_io_error_from_errno (errno),
                       "Error seeking to offset %" G_GUINT64_FORMAT ": %s",
                       offset, strerror (errno));
560 561
          goto out;
        }
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
    read_again:
      num_bytes_read = read (fd, buffer, size);
      if (num_bytes_read < 0)
        {
          if (errno == EAGAIN || errno == EINTR)
            goto read_again;
        }
      else
        {
          /* EOF */
          if (num_bytes_read == 0)
            {
              g_set_error (error,
                           G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Reading from offset %" G_GUINT64_FORMAT " returned zero bytes",
                           offset);
              goto out;
            }
        }
    }

  if (num_bytes_read < 0)
    {
      /* do not consider this an error - treat as zero bytes read */
      num_bytes_read = 0;
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
    }

  num_bytes_to_write = num_bytes_read;
  if (pad_with_zeroes && (guint64) num_bytes_read < size)
    {
      memset (buffer + num_bytes_read, 0, size - num_bytes_read);
      num_bytes_to_write = size;
    }

  if (!g_seekable_seek (G_SEEKABLE (output_stream),
                        offset,
                        G_SEEK_SET,
                        cancellable,
                        error))
    {
      g_prefix_error (error,
                      "Error seeking to offset %" G_GUINT64_FORMAT ": ",
                      offset);
      goto out;
    }
607

608 609 610 611 612 613
  if (!g_output_stream_write_all (G_OUTPUT_STREAM (output_stream),
                                  buffer,
                                  num_bytes_to_write,
                                  G_PRIORITY_DEFAULT,
                                  cancellable,
                                  error))
614
    {
615
      g_prefix_error (error,
616
                      "Error writing %" G_GSIZE_FORMAT " bytes to offset %" G_GUINT64_FORMAT ": ",
617 618
                      num_bytes_to_write,
                      offset);
619 620
      goto out;
    }
621 622 623

  ret = num_bytes_read;

624
 out:
625 626

  return ret;
627 628 629 630
}

/* ---------------------------------------------------------------------------------------------------- */

631 632
static gpointer
copy_thread_func (gpointer user_data)
633
{
634
  DialogData *data = user_data;
635
  GduDVDSupport *dvd_support = NULL;
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
  guchar *buffer_unaligned = NULL;
  guchar *buffer = NULL;
  guint64 block_device_size = 0;
  long page_size;
  GError *error = NULL;
  GError *error2 = NULL;
  gint64 last_update_usec = -1;
  gint fd = -1;
  gint buffer_size;
  guint64 num_bytes_completed = 0;

  /* default to 1 MiB blocks */
  buffer_size = (1 * 1024 * 1024);

  /* Most OSes put ACLs for logged-in users on /dev/sr* nodes (this is
   * so CD burning tools etc. work) so see if we can open the device
   * file ourselves. If so, great, since this avoids a polkit dialog.
   *
   * As opposed to udisks' OpenForBackup() we also avoid O_EXCL since
   * the disc is read-only by its very nature. As a side-effect this
   * allows creating a disk image of a mounted disc.
   */
  if (g_str_has_prefix (udisks_block_get_device (data->block), "/dev/sr"))
    {
660 661 662 663 664 665 666 667 668 669 670 671 672 673
      const gchar *device_file = udisks_block_get_device (data->block);
      fd = open (device_file, O_RDONLY);

      /* Use libdvdcss (if available on the system) on DVDs with UDF
       * filesystems - otherwise the backup process may fail because
       * of unreadable/scrambled sectors
       */
      if (g_strcmp0 (udisks_block_get_id_usage (data->block), "filesystem") == 0 &&
          g_strcmp0 (udisks_block_get_id_type (data->block), "udf") == 0 &&
          g_str_has_prefix (udisks_drive_get_media (data->drive), "optical_dvd"))
        {
          g_mutex_lock (&data->copy_lock);
          data->retrieving_dvd_keys = TRUE;
          g_mutex_unlock (&data->copy_lock);
674
          g_idle_add (on_update_job, dialog_data_ref (data));
675 676 677 678 679 680

          dvd_support = gdu_dvd_support_new (device_file, udisks_block_get_size (data->block));

          g_mutex_lock (&data->copy_lock);
          data->retrieving_dvd_keys = FALSE;
          g_mutex_unlock (&data->copy_lock);
681
          g_idle_add (on_update_job, dialog_data_ref (data));
682
        }
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
    }

  /* Otherwise, request the fd from udisks */
  if (fd == -1)
    {
      GUnixFDList *fd_list = NULL;
      GVariant *fd_index = NULL;
      if (!udisks_block_call_open_for_backup_sync (data->block,
                                                   g_variant_new ("a{sv}", NULL), /* options */
                                                   NULL, /* fd_list */
                                                   &fd_index,
                                                   &fd_list,
                                                   NULL, /* cancellable */
                                                   &error))
        goto out;

      fd = g_unix_fd_list_get (fd_list, g_variant_get_handle (fd_index), &error);
      if (error != NULL)
        {
          g_prefix_error (&error,
                          "Error extracing fd with handle %d from D-Bus message: ",
                          g_variant_get_handle (fd_index));
          goto out;
        }
      if (fd_index != NULL)
        g_variant_unref (fd_index);
      g_clear_object (&fd_list);
    }

  g_assert (fd != -1);
713 714 715 716 717

  /* We can't use udisks_block_get_size() because the media may have
   * changed and udisks may not have noticed. TODO: maybe have a
   * Block.GetSize() method instead...
   */
718
  if (ioctl (fd, BLKGETSIZE64, &block_device_size) != 0)
719
    {
720 721 722
      error = g_error_new (G_IO_ERROR, g_io_error_from_errno (errno),
                           "%s", strerror (errno));
      g_prefix_error (&error, _("Error determining size of device: "));
723 724 725
      goto out;
    }

726 727 728 729 730 731
  if (block_device_size == 0)
    {
      error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                           _("Device is size 0"));
      goto out;
    }
732

733 734
  /* If supported, allocate space at once to ensure blocks are laid
   * out contigously, see http://lwn.net/Articles/226710/
735 736 737 738 739 740 741 742 743 744 745
   */
  if (G_IS_FILE_DESCRIPTOR_BASED (data->output_file_stream))
    {
      gint output_fd = g_file_descriptor_based_get_fd (G_FILE_DESCRIPTOR_BASED (data->output_file_stream));
      gint rc;

      g_mutex_lock (&data->copy_lock);
      data->allocating_file = TRUE;
      g_mutex_unlock (&data->copy_lock);
      g_idle_add (on_update_job, dialog_data_ref (data));

746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
      rc = fallocate (output_fd,
                      0, /* mode */
                      (off_t) 0,
                      (off_t) block_device_size);

      if (rc != 0)
        {
          if (errno == ENOSYS || errno == EOPNOTSUPP)
            {
              /* If the kernel or filesystem does not support it, too
               * bad. Just continue.
               */
            }
          else
            {
              error = g_error_new (G_IO_ERROR, g_io_error_from_errno (errno), "%s", strerror (errno));
              g_prefix_error (&error, _("Error allocating space for disk image file: "));
              goto out;
            }
        }
766 767 768 769 770 771 772

      g_mutex_lock (&data->copy_lock);
      data->allocating_file = FALSE;
      g_mutex_unlock (&data->copy_lock);
      g_idle_add (on_update_job, dialog_data_ref (data));
    }

773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
  page_size = sysconf (_SC_PAGESIZE);
  buffer_unaligned = g_new0 (guchar, buffer_size + page_size);
  buffer = (guchar*) (((gintptr) (buffer_unaligned + page_size)) & (~(page_size - 1)));

  g_mutex_lock (&data->copy_lock);
  data->estimator = gdu_estimator_new (block_device_size);
  data->update_id = 0;
  data->num_error_bytes = 0;
  data->start_time_usec = g_get_real_time ();
  g_mutex_unlock (&data->copy_lock);

  /* Read huge (e.g. 1 MiB) blocks and write it to the output
   * file even if it was only partially read.
   */
  num_bytes_completed = 0;
  while (num_bytes_completed < block_device_size)
    {
      gssize num_bytes_to_read;
      gssize num_bytes_read;
      gint64 now_usec;

      num_bytes_to_read = buffer_size;
      if (num_bytes_to_read + num_bytes_completed > block_device_size)
        num_bytes_to_read = block_device_size - num_bytes_completed;

      /* Update GUI - but only every 200 ms and only if last update isn't pending */
      g_mutex_lock (&data->copy_lock);
      now_usec = g_get_monotonic_time ();
      if (now_usec - last_update_usec > 200 * G_USEC_PER_SEC / 1000 || last_update_usec < 0)
        {
          if (num_bytes_completed > 0)
            gdu_estimator_add_sample (data->estimator, num_bytes_completed);
          if (data->update_id == 0)
806
            data->update_id = g_idle_add (on_update_job, dialog_data_ref (data));
807 808 809 810 811 812 813 814 815 816
          last_update_usec = now_usec;
        }
      g_mutex_unlock (&data->copy_lock);

      num_bytes_read = copy_span (fd,
                                  G_OUTPUT_STREAM (data->output_file_stream),
                                  num_bytes_completed,
                                  num_bytes_to_read,
                                  buffer,
                                  TRUE, /* pad_with_zeroes */
817
                                  dvd_support,
818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836
                                  data->cancellable,
                                  &error);
      if (num_bytes_read < 0)
        goto out;

      /*g_print ("read %" G_GUINT64_FORMAT " bytes (requested %" G_GUINT64_FORMAT ") from offset %" G_GUINT64_FORMAT "\n",
               num_bytes_read,
               num_bytes_to_read,
               num_bytes_completed);*/

      if (num_bytes_read < num_bytes_to_read)
        {
          guint64 num_bytes_skipped = num_bytes_to_read - num_bytes_read;
          g_mutex_lock (&data->copy_lock);
          data->num_error_bytes += num_bytes_skipped;
          g_mutex_unlock (&data->copy_lock);
        }
      num_bytes_completed += num_bytes_to_read;
    }
837 838

 out:
839 840 841
  if (dvd_support != NULL)
    gdu_dvd_support_free (dvd_support);

842
  data->end_time_usec = g_get_real_time ();
843

844 845 846 847 848 849
  /* in either case, close the stream */
  if (!g_output_stream_close (G_OUTPUT_STREAM (data->output_file_stream),
                              NULL, /* cancellable */
                              &error2))
    {
      g_warning ("Error closing file output stream: %s (%s, %d)",
850
                 error2->message, g_quark_to_string (error2->domain), error2->code);
851 852 853
      g_clear_error (&error2);
    }
  g_clear_object (&data->output_file_stream);
854

855
  if (error != NULL)
856
    {
857
      /* show error in GUI */
858
      if (!(error->domain == G_IO_ERROR && error->code == G_IO_ERROR_CANCELLED))
859 860 861 862 863
        {
          data->copy_error = error; error = NULL;
          g_idle_add (on_show_error, dialog_data_ref (data));
        }
      g_clear_error (&error);
864

865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882
      /* Cleanup */
      if (!g_file_delete (data->output_file, NULL, &error))
        {
          g_warning ("Error deleting file: %s (%s, %d)",
                     error->message, g_quark_to_string (error->domain), error->code);
          g_clear_error (&error);
        }
    }
  else
    {
      /* success */
      g_idle_add (on_success, dialog_data_ref (data));
    }
  if (fd != -1 )
    {
      if (close (fd) != 0)
        g_warning ("Error closing fd: %m");
    }
883

884
  g_free (buffer_unaligned);
885

886 887
  dialog_data_unref_in_idle (data); /* unref on main thread */
  return NULL;
888 889
}

890 891
/* ---------------------------------------------------------------------------------------------------- */

892 893
/* returns TRUE if OK to overwrite or file doesn't exist */
static gboolean
894
check_overwrite (DialogData *data)
895 896 897 898 899 900 901 902 903
{
  GFile *folder = NULL;
  const gchar *name;
  gboolean ret = TRUE;
  GFile *file = NULL;
  GFileInfo *folder_info = NULL;
  GtkWidget *dialog;
  gint response;

904 905
  name = gtk_entry_get_text (GTK_ENTRY (data->name_entry));
  folder = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (data->folder_fcbutton));
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921
  file = g_file_get_child (folder, name);
  if (!g_file_query_exists (file, NULL))
    goto out;

  folder_info = g_file_query_info (folder,
                                   G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
                                   G_FILE_QUERY_INFO_NONE,
                                   NULL,
                                   NULL);
  if (folder_info == NULL)
    goto out;

  dialog = gtk_message_dialog_new (GTK_WINDOW (data->dialog),
                                   GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
                                   GTK_MESSAGE_QUESTION,
                                   GTK_BUTTONS_NONE,
922
                                   _("A file named “%s” already exists.  Do you want to replace it?"),
923 924
                                   name);
  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
925
                                            _("The file already exists in “%s”.  Replacing it will overwrite its contents."),
926
                                            g_file_info_get_display_name (folder_info));
927
  gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Cancel"), GTK_RESPONSE_CANCEL);
928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943
  gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Replace"), GTK_RESPONSE_ACCEPT);
  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  response = gtk_dialog_run (GTK_DIALOG (dialog));

  if (response != GTK_RESPONSE_ACCEPT)
    ret = FALSE;

  gtk_widget_destroy (dialog);

 out:
  g_clear_object (&folder_info);
  g_clear_object (&file);
  g_clear_object (&folder);
  return ret;
}

944 945 946 947 948 949 950 951 952 953 954 955 956
static void
on_local_job_canceled (GduLocalJob  *job,
                       gpointer      user_data)
{
  DialogData *data = user_data;
  if (!data->completed)
    {
      dialog_data_terminate_job (data);
      dialog_data_complete_and_unref (data);
      update_job (data, FALSE);
    }
}

957
static gboolean
958
start_copying (DialogData *data)
959 960 961 962 963 964
{
  gboolean ret = TRUE;
  const gchar *name;
  GFile *folder;
  GError *error;

965 966
  name = gtk_entry_get_text (GTK_ENTRY (data->name_entry));
  folder = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (data->folder_fcbutton));
967 968 969 970 971 972 973 974 975 976 977

  error = NULL;
  data->output_file = g_file_get_child (folder, name);
  data->output_file_stream = g_file_replace (data->output_file,
                                             NULL, /* etag */
                                             FALSE, /* make_backup */
                                             G_FILE_CREATE_NONE,
                                             NULL,
                                             &error);
  if (data->output_file_stream == NULL)
    {
978 979
      gdu_utils_show_error (GTK_WINDOW (data->dialog), _("Error opening file for writing"), error);
      g_clear_error (&error);
980
      g_object_unref (folder);
981
      dialog_data_complete_and_unref (data);
982 983 984 985
      ret = FALSE;
      goto out;
    }

986
  /* now that we know the user picked a folder, update file chooser settings */
987
  gdu_utils_file_chooser_for_disk_images_set_default_folder (folder);
988

989 990 991 992 993 994 995
  data->inhibit_cookie = gtk_application_inhibit (GTK_APPLICATION (gdu_window_get_application (data->window)),
                                                  GTK_WINDOW (data->dialog),
                                                  GTK_APPLICATION_INHIBIT_SUSPEND |
                                                  GTK_APPLICATION_INHIBIT_LOGOUT,
                                                  /* Translators: Reason why suspend/logout is being inhibited */
                                                  C_("create-inhibit-message", "Copying device to disk image"));

996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
  data->local_job = gdu_application_create_local_job (gdu_window_get_application (data->window),
                                                      data->object);
  udisks_job_set_operation (UDISKS_JOB (data->local_job), "x-gdu-create-disk-image");
  /* Translators: this is the description of the job */
  gdu_local_job_set_description (data->local_job, _("Creating Disk Image"));
  udisks_job_set_progress_valid (UDISKS_JOB (data->local_job), TRUE);
  udisks_job_set_cancelable (UDISKS_JOB (data->local_job), TRUE);
  g_signal_connect (data->local_job, "canceled",
                    G_CALLBACK (on_local_job_canceled),
                    data);

  dialog_data_hide (data);

1009 1010 1011
  g_thread_new ("copy-disk-image-thread",
                copy_thread_func,
                dialog_data_ref (data));
1012 1013 1014 1015 1016 1017

 out:
  g_clear_object (&folder);
  return ret;
}

1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
static void
ensure_unused_cb (GduWindow     *window,
                  GAsyncResult  *res,
                  gpointer       user_data)
{
  DialogData *data = user_data;
  if (gdu_window_ensure_unused_finish (window, res, NULL))
    {
      start_copying (data);
    }
  else
    {
      dialog_data_complete_and_unref (data);
    }
}

1034 1035 1036 1037 1038
static void
on_dialog_response (GtkDialog     *dialog,
                    gint           response,
                    gpointer       user_data)
{
1039 1040 1041
  DialogData *data = user_data;

  switch (response)
1042
    {
1043
    case GTK_RESPONSE_OK:
1044 1045
      if (check_overwrite (data))
        {
1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
          /* If it's a optical drive, we don't need to try and
           * manually unmount etc.  everything as we're attempting to
           * open it O_RDONLY anyway - see copy_thread_func() for
           * details.
           */
          if (g_str_has_prefix (udisks_block_get_device (data->block), "/dev/sr"))
            {
              start_copying (data);
            }
          else
            {
              /* ensure the device is unused (e.g. unmounted) before copying data from it... */
              gdu_window_ensure_unused (data->window,
                                        data->object,
                                        (GAsyncReadyCallback) ensure_unused_cb,
                                        NULL, /* GCancellable */
                                        data);
            }
1064
        }
1065 1066 1067 1068 1069 1070
      break;

    case GTK_RESPONSE_CLOSE:
      dialog_data_complete_and_unref (data);
      break;

1071 1072 1073 1074
    default: /* explicit fallthrough */
    case GTK_RESPONSE_CANCEL:
      dialog_data_complete_and_unref (data);
      break;
1075 1076 1077
    }
}

1078 1079 1080 1081
void
gdu_create_disk_image_dialog_show (GduWindow    *window,
                                   UDisksObject *object)
{
1082 1083
  DialogData *data;
  guint n;
1084

1085
  data = g_new0 (DialogData, 1);
1086
  data->ref_count = 1;
1087
  g_mutex_init (&data->copy_lock);
1088 1089 1090 1091 1092
  data->window = g_object_ref (window);
  data->object = g_object_ref (object);
  data->block = udisks_object_get_block (object);
  g_assert (data->block != NULL);
  data->drive = udisks_client_get_drive_for_block (gdu_window_get_client (window), data->block);
1093
  data->cancellable = g_cancellable_new ();
1094

1095 1096 1097 1098
  data->dialog = GTK_WIDGET (gdu_application_new_widget (gdu_window_get_application (window),
                                                         "create-disk-image-dialog.ui",
                                                         "create-disk-image-dialog",
                                                         &data->builder));
1099 1100 1101 1102 1103 1104
  for (n = 0; widget_mapping[n].name != NULL; n++)
    {
      gpointer *p = (gpointer *) ((char *) data + widget_mapping[n].offset);
      *p = gtk_builder_get_object (data->builder, widget_mapping[n].name);
    }
  g_signal_connect (data->name_entry, "notify::text", G_CALLBACK (on_notify), data);
1105 1106

  create_disk_image_populate (data);
1107
  create_disk_image_update (data);
1108 1109 1110

  gtk_dialog_set_default_response (GTK_DIALOG (data->dialog), GTK_RESPONSE_OK);

1111 1112 1113 1114
  data->response_signal_handler_id = g_signal_connect (data->dialog,
                                                       "response",
                                                       G_CALLBACK (on_dialog_response),
                                                       data);
1115

1116
  gtk_window_set_transient_for (GTK_WINDOW (data->dialog), GTK_WINDOW (window));
1117
  gtk_window_present (GTK_WINDOW (data->dialog));
1118 1119 1120 1121

  /* Only select the precomputed filename, not the .img / .iso extension */
  gtk_editable_select_region (GTK_EDITABLE (data->name_entry), 0,
                              strlen (gtk_entry_get_text (GTK_ENTRY (data->name_entry))) - 4);
1122
}